Java單元測試框架與實踐
本文首先在理論上歸納了單元測試在宏觀和微觀層面要遵循的基本原則,以及測試覆蓋率的要求和評價維度。然後具體闡述了筆者實戰中總結的基於Junit + Mockito 的單元測試框架和具體實施方法,並給出了相應的demo代碼。
本文主要參考和引用了《碼出高效:Java開發手冊》,Junit5、mockito等官方文檔以及若干篇相關博客的內容,具體可見文末參考鏈接部分。
基本原則
宏觀層面:AIR原則
- A:Automatic(自動化)
全自動執行,輸出結果無需人工檢查,而是通過斷言驗證。 - I:Independent(獨立性)
分層測試,各層之間不相互依賴。 - R:Repeatable(可重複)
可重複執行,不受外部環境( 網絡、服務、中間件等)影響。
微觀層面:BCDE原則
- B: Border,邊界值測試,包括循環邊界、特殊取值、特殊時間點、數據順序等。
- C: Correct,正確的輸入,並得到預期的結果。
- D: Design,與設計文檔相結合,來編寫單元測試。
- E : Error,單元測試的目標是證明程序有錯,而不是程序無錯。爲了發現代碼中潛在的錯誤, 我們需要在編寫測試用例時有一些強制的錯誤輸入(如非法數據、異常流程、非業務允許輸入等)來得到預期的錯誤結果。
Mock
由於單元測試只是系統集成測試前的小模塊測試,有些因素往往是不具備的,因此需要進行Mock。例如:
- 功能因素。比如被測試方法內部調用的功能不可用。
- 時間因素。比如雙十一還沒有到來,與此時間相關的功能點。
- 環境因素。政策環境,如支付寶政策類新功能,多端環境, 如PC 、手機等。
- 數據因素。線下數據樣本過小,難以覆蓋各種線上真實場景。
覆蓋率要求
- 粗粒度覆蓋:類覆蓋率和方法覆蓋率應達到100%。
- 細粒度覆蓋:行覆蓋、分支覆蓋、條件判定覆蓋、路徑覆蓋等,相應的覆蓋率應考慮上述原則與因素。
測試框架
底層測試框架:junit
Junit主流還是junit4,最新版本是4.12(2014年12月5日),現在最新的是junit5(JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage)。junit5正式版本的發佈日期是2017年9月11日,目前最新的版本是5.5.2(2019年9月9日)。我們項目底層選擇了junit5。
Mock工具
- 方法:mockito
目前,在 Java 陣營中主要的 Mock 測試工具有 Mockito、JMock、EasyMock 等。我們選擇了功能更強大且容易上手的Mockito。
另外,Mockito不支持static的的方法的mock,要使用PowerMock來模擬。但是PowerMock似乎現在還不支持junit5,我們沒有使用。 - 模擬數據生成:jmockdata
- Redis:redis-mock
- 數據庫:h2
- 接口:MockMVC
測試實施方法
該部分以Spring Boot項目爲例,介紹單元測試中主要碰到的需求與問題以及相應的實施方法。開發環境選擇IntelliJ IDEA 2018。
測試需求分析
原則 | DAO層 | service層 | controller層 |
---|---|---|---|
Automatic | 底層測試框架 | ||
Independent/Repeatable | Mock DB | Mock DAO層接口、第三方API | Mock service層接口、Restful請求 |
Border/Error | 參數化測試;重複測試;條件測試;分類測試;模擬數據生成 | ||
Correct | 斷言 | ||
Design | 測試報告自動生成 |
Junit5的基本使用
Maven導入依賴
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
常用註解
最基本的測試代碼
import com.demo.entity.User;
import com.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class UserTest {
@Autowired
private UserService userService;
@Test
void userSelectByName(String userName) {
User user = userService.selectByName(userName);
assertNotNull(user);
}
}
參數化測試
使用@ParameterizedTest註解,參數化測試使得測試可以測試多次使用不同的參數值。
參數源:
@ValueSource是最簡單的來源之一。它允許你指定單個數組的文字值,並且只能用於爲每個參數化的測試調用提供單個參數。
@NullSource註解用來給參數測試提供一個null元素,要求傳參的類型不能是基本類型(基本類型不能是null值)
@EmptySource爲java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays等類型提供了空參數。
@NullAndEmptySource 註解爲上邊兩個註解的合併。
@EnumSource能夠很方便地提供Enum常量。該註解提供了一個可選的names參數,你可以用它來指定使用哪些常量。如果省略了,就意味着所有的常量將被使用。
@SpringBootTest
public class UserTest {
@Autowired
private UserService userService;
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"張三", "Thomas", "000001"})
void userSelectByName(String userName) {
User user = userService.selectByName(userName);
assertNotNull(user);
}
}
重複測試
@RepeatedTest中填入次數可以重複測試。
@SpringBootTest
public class UserTest {
@Autowired
private UserService userService;
@RepeatedTest(3)
void userSelectByName(String userName) {
User user = userService.selectByName(userName);
assertNotNull(user);
}
}
條件測試
//禁用測試
@Disabled("Disabled until bug #99 has been fixed")
void userSelectByName(String userName) {}
//操作系統條件
@EnabledOnOs(MAC)
@DisabledOnOs(WINDOWS)
void userSelectByName(String userName) {}
//腳本條件
@EnabledIf("2 * 3 == 6")
@DisabledIf("Math.random() < 0.314159")
void userSelectByName(String userName) {}
// 基於標籤過濾
@Tag("fast")
@Tag("model")
void userSelectByName(String userName) {}
測試執行順序
可以在類上標註@TestMethodOrder來聲明測試方法要有執行順序,裏邊可以傳入三種類Alphanumeric、OrderAnnotation、Random,分別代表字母排序、數字排序、隨機。然後對方法加@Order註解裏邊傳入參數決定順序。
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
@TestMethodOrder(OrderAnnotation.class)
@SpringBootTest
public class UserTest {
@Autowired
private UserService userService;
@Test
@Order(1)
void userSelectByName(String userName) {
User user = userService.selectByName(userName);
assertNotNull(user);
}
@Test
@Order(2)
void method2() {}
@Test
@Order(3)
void method3() {}
}
斷言
所有的JUnit Jupiter斷言都是 org.junit.jupiter.api.Assertions類中static方法。可以使用Lambda表達式。
然後如果斷言不能滿足要求,可以導入第三方的斷言庫。
動態測試
JUnit Jupiter還引入了一種全新的測試編程模型。
這種新類型的測試是一種動態測試,它是由一個工廠方法在運行時生成的,該方法用@TestFactory註釋。與@Test方法相比,@TestFactory方法本身不是測試用例,而是測試用例的工廠。 因此,動態測試是工廠的產物。
同一個@TestFactory所生成的n個動態測試,@BeforeEach和@AfterEach只會在這n個動態測試開始前和結束後各執行一次,不會爲每一個單獨的動態測試都執行。
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
Mockito的基本使用
Maven導入依賴
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
基於Mockito的service層測試
import com.demo.entity.User;
import com.demo.service.UserService;
import com.demo.dao.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@SpringBootTest
public class UserTest {
@Mock //將生成MockDao,並注入到@InjectMocks指定的類中
private UserRepository userRepository;
@InjectMocks //使用Mockito的@InjectMocks註解將待測試的實現類注入
private UserService userService;
@Test
void userSelectByName(String userName) {
User userMock = new User();
userMock.setUserName("張三");
when(userRepository.selectByName(any()).thenReturn(userMock);
User user = userService.selectByName(userName);
assertNotNull(user);
}
}
controller層測試
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void userSelectByName(String userName) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Content-Type", CONTENT_TYPE_TEST);
headers.add("Authorization", AUTHORIZATION_TEST);
ResponseEntity responseEntity = restTemplate.exchange("/user?name=Thomas", HttpMethod.GET, new HttpEntity<Object>(headers), JSONObject.class);
System.out.println(responseEntity.getBody());
assertThat(HttpStatus.OK, equalTo(responseEntity.getStatusCode());
}
}
DAO層測試
@Autowired
private UserRepository userRepository;
@Test
@Transactional
@Rollback(true)// 事務自動回滾,默認是true。可以不寫
public void insertUser(){
User user = new User();
assertNotNull(userRepository.insert(user));
}
模擬數據生成
JmockData 2.0:https://github.com/jsonzou/jmockdata-demo
測試報告生成
命名規範
-
測試方法命名
(1)原方法 or 原方法+Test
(2) test + 待測場景和期待結果的命名方式
例如,testDecodeUserTokenSuccess
(3)“should . … When”結構
例如,shouldSuccessWhenDecodeUserToken -
- 測試類和方法名稱呈現
@DisplayName
- 測試類和方法名稱呈現
覆蓋率統計
cobertura:https://www.jianshu.com/p/159880556d6c
報告美化
《JUnit報告美化——ExtentReports》:https://www.cnblogs.com/goldsking/p/7598085.html
參考資料
[1]《碼出高效:Java開發手冊》:https://book.douban.com/subject/30333948/
[2] 《JUnit 5 User Guide》:https://junit.org/junit5/docs/current/user-guide
[3] JUnit5用戶手冊:https://www.cnblogs.com/followerofjests/p/10466070.html
[4] 單元測試實踐(SpringCloud+Junit5+Mockito+DataMocker):https://www.cnblogs.com/pluto4596/p/11703382.html
[5] Mockito 中文文檔 ( 2.0.26 beta ):https://github.com/hehonghui/mockito-doc-zh