【轉】Java JUnit 單元測試小結
原文鏈接:https://segmentfault.com/a/1190000006731125
測試類型
單元測試(Unit test)
單元測試關註單一的類. 它們存在的目的是檢查這個類中的代碼是否按照期望正確運行.
集成測試(Integration test)
顧名思義, 集成測試是檢查開發的模塊和其他模塊整合時是否正常工作.
雖然集成測試的代碼影響範圍比單元測試要廣, 但是集成測試和單元測試一樣, 也是針對於開發者而言的.
端到端測試(End-to-End test)
端到端測試是將整個系統作為一個整體, 然後從用戶的角度進行測試的.
端到端測試的目的是測試系統在實際使用的是否正常的, 因此通常來說是不需要測試替身的(Test Double)
單元測試基本概念
什麽是單元測試
單元測試的目的: 測試當前所寫的代碼是否是正確的, 例如輸入一組數據, 會輸出期望的數據; 輸入錯誤數據, 會產生錯誤異常等.
在單元測試中, 我們需要保證被測系統是獨立的(SUT 沒有任何的 DOC), 即當被測系統通過測試時, 那麽它在任何環境下都是能夠正常工作的. 編寫單元測試時, 僅僅需要關註單個類就可以了.
而不需要關註例如數據庫服務, Web 服務等組件.
被測系統
被測系統(System under test, SUT)表示正在被測試的系統, 目的是測試系統能否正確操作.
根據測試類型的不同, SUT 指代的內容也不同, 例如 SUT 可以是一個類甚至是一整個系統.
測試依賴組件(DOC)
被測系統所依賴的組件, 例如進程 UserService 的單元測試時, UserService 會依賴 UserDao, 因此 UserDao 就是 DOC.
測試替身(Test Double)
一個實際的系統會依賴多個外部對象, 但是在進行單元測試時, 我們會用一些功能較為簡單的並且其行為和實際對象類似的假對象來作為 SUT 的依賴對象, 以此來降低單元測試的復雜性和可實現性. 在這裏, 這些假對象就被稱為 測試替身(Test Double).
測試替身有如下 5 種類型:
-
Test stub, 為 SUT 提供數據的假對象.
我們舉一個例子來展示什麽是 Test stub.
假設我們的一個模塊需要從 HTTP 接口中獲取商品價格數據, 這個獲取數據的接口被封裝為 getPrice 方法. 在對這個模塊進行測試時, 我們顯然不太可能專門開一個 HTTP 服務器來提供此接口, 而是提供一個帶有 getPrice 方法的假對象, 從這個假對象中獲取數據.
在這個例子中, 提供數據的假對象就叫做 Test stub.
-
Fake object
實現了簡單功能的一個假對象. Fake object 和 Test stub 的主要區別就是 Test stub 側重於用於提供數據的假對象, 而 Fake object 沒有這層含義.
使用 Fake object 的最主要的原因就是在測試時某些組件不可用或運行速度太慢, 因而使用 Fake object 來代替它們.
-
Mock object
用於模擬實際的對象, 並且能夠校驗對這個 Mock object 的方法調用是否符合預期.
實際上, Mock object 是 Test stub 或 Fake object 一種, 但是 Mock object 有 Test stub/Fake object 沒有的特性, Mock object 可以很靈活地配置所調用的方法所產生的行為, 並且它可以追蹤方法調用, 例如一個 Mock Object 方法調用時傳遞了哪些參數, 方法調用了幾次等.
-
Dummy object: 在測試中並不使用的, 但是為了測試代碼能夠正常編譯/運行而添加的對象. 例如我們調用一個 Test Double 對象的一個方法, 這個方法需要傳遞幾個參數, 但是其中某個參數無論是什麽值都不會影響測試的結果, 那麽這個參數就是一個 Dummy object.
Dummy object 可以是一個空引用, 一個空對象或者是一個常量等.
簡單的說, Dummy object 就是那些沒有使用到的, 僅僅是為了填充參數列表的對象.
-
Test Spy
可以包裝一個真實的 Java 對象, 並返回一個包裝後的新對象. 若沒有特別配置的話, 對這個新對象的所有方法調用, 都會委派給實際的 Java 對象.
mock 和 spy 的區別是: mock 是無中生有地生出一個完全虛擬的對象, 它的所有方法都是虛擬的; 而 spy 是在現有類的基礎上包裝了一個對象, 即如果我們沒有重寫 spy 的方法, 那麽這些方法的實現其實都是調用的被包裝的對象的方法.
Test fixture
所謂 test fixture, 就是運行測試程序所需要的先決條件(precondition). 即對被測對象進行測試時鎖需要的一切東西(The test fixture is everything we need to have in place to exercise the SUT). 這個 東西 不單單指的是數據, 同時包括對被測對象的配置, 被測對象所需要的依賴對象等.
JUnit4 之前是通過 setUp, TearDown 方法完成, 在 JUnit4這, 我們可以使用@Before 代替 setUp 方法, @After 代替 tearDown 方法.
註意
, @Before 在每個測試方法運行前都會被調用, @After 在每個測試方法運行後都會被調用
因為 @Before 和 @After 會在每個測試方法前後都會被調用, 而有時我們僅僅需要在測試前進行一次初始化, 這樣的情況下, 可以使用@BeforeClass 和@AfterClass 註解.
測試用例(Test case)
在 JUnit 3中, 測試方法都必須以 test 為前綴, 且必須是 public void 的, JUnit 4之後, 就沒有這個限制了, 只要在每個測試方法標註 @Test 註解, 方法簽名可以是任意的.
測試套件
通過 TestSuit 對象將多個測試用例組裝成一個測試套件, 測試套件批量運行.
通過@RunWith 和@SuteClass 兩個註解, 我們可以創建一個測試套件. 通過@RunWith 指定一個特殊的運行器, 幾 Suite.class 套件運行器, 並通過@SuiteClasses 註解, 將需要進行測試的類列表作作為參數傳入.
JUnit4
HelloWorld 例子
我們已一個簡單的例子來快速展示 JUnit4 的基本用法.
首先新建一個名為 JUniTest 的 Maven 工程, 然後添加依賴:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
接著編寫測試套件:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TestJunit {
@Test
public void testingCrunchifyAddition() {
assertEquals("Here is test for Addition Result: ", 30, addition(27, 3));
}
@Test
public void testingHelloWorld() {
assertEquals("Here is test for Hello World String: ", "Hello + World", helloWorld());
}
public int addition(int x, int y) {
return x + y;
}
public String helloWorld() {
String helloWorld = "Hello +" + " World";
return helloWorld;
}
}
隨後使用測試用例:
public class App {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(TestJunit.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
if (result.wasSuccessful()) {
System.out.println("Both Tests finished successfully...");
}
}
}
這就是一個完整的 JUnit 測試例子了.
定義測試
一個 JUnit 測試是一個在專用於測試的類中的一個方法, 並且這個方法被 @org.junit.Test 註解標註. 例如:
public class TestJunit {
@Test
public void testingCrunchifyAddition() {
assertEquals("Here is test for Addition Result: ", 30, addition(27, 3));
}
...
}
JUnit4 生命周期
JUnit4測試用例的完整的生命周期要經歷如下幾個階段:
-
類級初始化資源處理
-
方法級初始化資源處理
-
執行測試用例中的方法
-
方法級銷毀資源處理
-
類級銷毀資源處理
其中, 類級初始化和銷毀資源處理在每一個測試用例類這僅僅執行一次, 方法級初始化, 銷毀資源處理方法在執行測試用例這的每個測試方法中都會被執行一次.
JUnit4 註解
-
@Test (expected = Exception.class) 表示預期會拋出Exception.class 的異常
-
@Ignore 含義是“某些方法尚未完成,暫不參與此次測試”。這樣的話測試結果就會提示你有幾個測試被忽略,而不是失敗。一旦你完成了相應函數,只需要把@Ignore註解刪去,就可以進行正常的測試。
-
@Test(timeout=100) 表示預期方法執行不會超過 100 毫秒,控制死循環
-
@Before 表示該方法在每一個測試方法之前運行,可以使用該方法進行初始化之類的操作
-
@After 表示該方法在每一個測試方法之後運行,可以使用該方法進行釋放資源,回收內存之類的操
-
@BeforeClass 表示該方法只執行一次,並且在所有方法之前執行。一般可以使用該方法進行數據庫連接操作,註意該註解運用在靜態方法。
-
@AfterClass 表示該方法只執行一次,並且在所有方法之後執行。一般可以使用該方法進行數據庫連接關閉操作,註意該註解運用在靜態方法。
@Test 註解
被@Test 標註的方法就是執行測試用例的測試方法, 例如:
public class TestJunit {
@Test
public void myTest() {
assertEquals("Here is test for Addition Result: ", 30, addition(27, 3));
}
}
方法myTest 被註解@Test 標註, 表示這個方法是一個測試方法, 當運行測試用例時, 會自動調用這個方法 .
@BeforeClass , @AfterClass, @Before, @After
使用@BeforeClass 和 @AfterClass 兩個註解標註的方法會在所有測試方法執行前後各執行一次
使用@Before 和 @After 兩個註解標註的方法會在每個測試方法執行前後都執行一次.
TestSuite
如果有多個測試類, 可以合並成一個測試套件進行測試, 運行一個 Test Suite, 那麽就會運行在這個 Test Suite 中的所用的測試.
例如:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith( Suite.class )
@SuiteClasses( { JUnitTest1.class, JUnitTest2.class } )
public class AllTests {
}
在這個例子中, 我們定義了一個 Test Suite, 這個 Test Suite 包含了兩個測試類: JUnitTest1 和 JUnitTest2, 因此運行 這個 Test Suite 時, 就會自動運行這兩個測試類了.
在 IntelliJ IDEA 中使用 JUnit 4
創建一個名為 JUnitTest 的 HelloWorld 工程, 添加依賴:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
然後創建 src/tests, 在File -> Project Structure -> Modules -> Sources 中, 右鍵選中 tests, 將其設置為 Test, 此時 tests 目錄就變為綠色:
然後創建需要進行測試的類:
public class HelloWorld {
public String sayHello() {
return "Hello World!";
}
}
在 HelloWorld 類名上按下 alt + enter 後, 就可以自動生成測試類了:
IntelliJ 在生tests/ 目錄下生成了一個測試類, 我們可以添加自動測試內容:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class HelloWorldTest {
@Test
public void testSayHello() throws Exception {
HelloWorld helloWorld = new HelloWorld();
assertEquals(helloWorld.sayHello(), "Hello World!");
}
}
【轉】Java JUnit 單元測試小結