我是如何進行單元測試的

中發現大家都知道單元測試,也知道 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語句(如assertEqualsassertNotSameassertTrueassertNotNull等),assertThat語法如下

assertThat( [value], [matcher statement] );

相比於assertion,使用assertThat語法有以下優點

  1. 代碼風格統一
    assertThat可以替代所有的assertion語句,這樣可以在所有的單元測試中只使用一個斷言方法,使得編寫測試用例變得簡單,代碼風格變得統一,測試代碼也更容易維護

  2. 支持強大的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"))); 
  1. 更加易懂的日誌信息
    相比assertion語句,asserThat提供的錯誤信息更加易懂,便於排查問題,同樣是判空斷言不通過,assertion
assertNotNull(userMapper.selectById(3));

assertion語句報錯信息

從報錯信息只能看出是AssertionError,無法知道執行結果與期望值

assertThat(userMapper.selectById(3),notNullValue());

assertThat語句報錯信息

不僅說明了錯誤類型是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 層合併進行測試

掃碼關注我
一起學習,一起進步

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章