中發現大家都知道單元測試,也知道 junit,但是沒有人知道怎麼寫 junit 單元測試,在這裏分享我在工作中是怎麼寫單元測試的,供大家參考
什麼是單元測試
首先講講什麼是單元測試,單元測試是指對軟件中的最小可測試單元進行檢查和驗證。單元測試在質量保證中是非常重要的環節,根據測試金字塔模型,越往上層的測試,所需的測試投入比例越大,效果也越差,而單元測試的成本要小的多,也更容易發現問題
單元測試的過程
完整的單元測試包括上面幾個過程
數據準備
某些方法需要數據庫初始化一些數據才能正常執行(如獲取公衆號配置信息,公衆號配置信息在項目初始化的時候插如到數據庫中的),在執行單元測試時,經常遇到由於所依賴的數據不存在或被修改了,或者在新的環境下,所依賴的數據庫不存在,進而導致單元測試不通過
針對數據不存在/修改的情況,需要在執行測試用例前,初始化需要依賴到的一些數據,一般是數據庫數據。而對於數據庫不存在的情況,則使用H2
內存數據庫來模擬mysql
環境來解決
導入H2
依賴
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
<scope>test</scope>
</dependency>
/src/test/resources/application.yml 配置H2數據庫
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:mmall;MODE=MySQL
# 表結構初始化腳本,多個用逗號分割
schema: classpath:db/user_schemas.sql
/src/test/resources/user_schemas.sql
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶id',
`username` varchar(50) NOT NULL COMMENT '用戶名',
`password` varchar(50) NOT NULL COMMENT '用戶密碼',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
單元測試代碼
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
// 使用Transaction清理數據
@Transaction
public void selectByIdTest(){
// 數據準備
User user = new User();
user.setId(1);
user.setUsername("張三");
user.setPassword("123");
userService.save(user);
// 執行測試+結果驗證
assertNotNull(userService.selectById(1));
}
}
有時候
H2
並不能完全模擬mysql
,因爲某些mysql
特性/函數在H2
中並沒有,就得專門搭一個測試用的mysql
來跑單元測試,爲避免這種情況導致誤診,有條件的情況下建議搭一個測試用mysql
參數構造
構造調用被測方法需要傳入的參數
執行測試
這一步比較簡單,即執行被測方法
驗證結果
驗證結果返回值的正確性,統一使用junit4.4
提供的assertThat
斷言語法,不再使用之前的assertion
語句(如assertEquals
、assertNotSame
、assertTrue
、assertNotNull
等),assertThat
語法如下
assertThat( [value], [matcher statement] );
相比於assertion
,使用assertThat
語法有以下優點
-
代碼風格統一
assertThat
可以替代所有的assertion
語句,這樣可以在所有的單元測試中只使用一個斷言方法,使得編寫測試用例變得簡單,代碼風格變得統一,測試代碼也更容易維護 -
支持強大的
Matcher
匹配符
Matcher
匹配符具有很強的易讀性,使用起來更加靈活,如:想判斷某個字符串 s 是否含有子字符串"developer"
或"Works"
中間的一個
// JUnit 4.4 以前的版本:
assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
// JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works")));
- 更加易懂的日誌信息
相比assertion
語句,asserThat
提供的錯誤信息更加易懂,便於排查問題,同樣是判空斷言不通過,assertion
assertNotNull(userMapper.selectById(3));
從報錯信息只能看出是
AssertionError
,無法知道執行結果與期望值
assertThat(userMapper.selectById(3),notNullValue());
不僅說明了錯誤類型是
AssertionError
,而且還打印出了期望值和執行結果,更容易排查問題
數據清理
清理第一步準備的數據,有可能影響到下一步的測試
對於支持回滾的數據庫(如mysql),可使用@Transactional
註解回滾數據,對於不支持回滾操作的(如redis)則需要在測試方法的最後手動清理
使用Mock框架進行測試
很多情況下,尤其在微服務架構下,被測方法往往會調用一些第三方服務,這時候當依賴的第三方服務不穩定,就會導致單元測試執行失敗。這時候就需要對依賴的第三方服務進行mock
,使其返回正確的結果
class UserServiceImpl implements UserService {
private final AudienceClient audienceClient;
public UserServiceImpl(AudienceClient audienceClient){
this.audienceClient = audienceClient;
}
public User get(Long id) {
AudienceModel audienceModel = AudienceClient.getById(1);
// 省略...
return user;
}
}
@SpringBootTest
@RunWith(SpringRunner.class)
class UserServiceImplTest {
@InjectMocks // 注入 UserServiceImpl bean
private UserServiceImpl userService;
@Mock // mock掉AudienceClient
private AudienceClient audienceClient;
@Test
void get() {
AudienceModel model = new AudienceModel();
model.setId(1);
model.setNickName("hxy");
// mock 掉audienceClient 方法,使其放回預期結果
Mockito.when(audienceClient.getById(1)).thenReturn(model);
assertThat(userService.get(1),notNullValue());
}
}
項目代碼一般分爲 controller、service、dao 三層。在測試 controller 的時候,由於 controller 會調用 service,service 又會調用 dao 層。當我們測試 controller 的時候,往往由於 service 層的報錯導致 controller 測試不通過
controller 層主要測試參數校驗邏輯,測試的時候並不需要真正的調用 service 層服務,可以通過 mock 框架將 service 方法 mock 掉
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {
private MockMvc mvc;
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@Before
public void init() {
mvc = MockMvcBuilders.standaloneSetup(userController)
.build();
}
@Test
public void get() throws Exception {
Mockito.when(userService.get(1)).thenReturn(new User());
mvc.perform(MockMvcRequestBuilders.get("/user/get?id=1")
.characterEncoding("utf-8")
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
// .andExpect() 根據需要可添加多個andExpect
.andDo(MockMvcResultHandlers.print());
}
}
service 層主要測試業務邏輯是否正確,當 service 層發生調用外部服務的時候,需要 mock 掉外部服務的調用代碼,避免單元測試的時候調用外部服務,導致外部服務異常。同時單元測試不應該依賴外部服務
@SpringBootTest
@RunWith(SpringRunner.class)
class UserServiceImplTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private AudienceClient audienceClient;
@Test
void get() {
Mockito.when(audienceClient.selectById(1)).thenReturn(new User());
assertThat(userService.get(1),notNullValue());
}
}
service 層調用 dao 層,無需 mock 掉 dao層,因爲使用 mybatis-plus 有很多代碼是寫在 service 層的,dao 層無法單獨抽出來測試,所以可以將 service 層跟 dao 層合併進行測試