Java單元測試實戰


在一個項目開發中我們通常都是分工合作共同開發的,那么在業務中各個模塊可能會存在相互調用的情況。如果我們調用的某個模塊開發的同學還未開發完成,那么在進行單元測試的時候該如何辦呢?或者是我們只是想測試某個業務的邏輯代碼,不需要去連接那些基礎組件(比如數據庫這些)時,又應該如何做呢?再比如我們只想測試在某種情況下會自己的邏輯代碼是否正確,此時又該如何做呢?

當然你可能會想到直接去將相關的代碼寫死即可,但是萬一改動的地方比較多就很麻煩了;同時有的地方你改為死數據時,很可能待會兒你提交代碼時就會忘記,最后可能就會直接發布到正式環境里面去了。雖然直接寫死的方式效率很快,但是也容易發生錯誤;因此mock就是用來解決這些問題的,將mock和單元測試搭配后我們就可以輕松進行各個模塊的測試工作了(當然也會花費更多的步驟)。下面我們將通過一些示例來帶你快速了解如何在單元測試中使用mock。

注意:如果你公司的業務需求變更非常快,那么不建議寫單元測試,因為可能你的單測還沒寫完,需求就已經發生變更了。

依賴的包說明

因為我們目前的開發基本都是基於spring-boot來的。因此需要添加相關的依賴包:

<!--單元測試需要的包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>
<!--要進行靜態方法mock時需要引入powermock的依賴包-->
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

一些常用測試注解說明

  • @AutoConfigureMockMvc 該注解表示 MockMvc由spring容器構建,你只負責注入之后用就可以了。這種寫法是為了讓測試在Spring容器環境下執行。
  • @Mock 會虛擬一個對象,在使用它創建對象的方法時將不會真正執行,如果沒有寫when().thenReturn()語句時將直接返回null;即可以理解為沒有匹配的when().thenReturn()時都會返回null。同時這個注解相當於:Mockito.mock(xxx.class)來手動創建
  • @Spy 這和@Mock的區別是,它會實際的執行代碼邏輯。
  • @InjectMocks這個會自己將注入類中相關依賴的對自動模擬

單元測試示例

下面我們將舉例常用的場景單元測試的示例。示例中使用的是 spring-boot:2.5.6

測試路由層Controller

測試路由層主要就是一個模擬的請求,這樣便於我們在以后更新維護后,可以方便進行回歸測試。

示例1:測試路由層Controller,使用mock模擬請求

@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
@AutoConfigureMockMvc
public class MockMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void apiGETest() throws Exception {
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/hotel/detail?id=1"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn();

        mvcResult.getResponse().setCharacterEncoding("UTF-8");
        System.out.println(mvcResult.getResponse().getContentAsString());
    }
}

示例2:測試路由層Controller,使用mock模擬業務邏輯service層

@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
@AutoConfigureMockMvc
public class HotelControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private HotelService hotelService;

    @Test
    public void findHotel() throws Exception {

        Hotel hotel = new Hotel();
        hotel.setId(1);
        hotel.setHotelName("世外桃源酒店");
        hotel.setRoomNum(20);
        hotel.setPrice(new BigDecimal("120.90"));

        BDDMockito.given(this.hotelService.findById(ArgumentMatchers.anyInt())).willReturn(hotel);

        // 這里是模擬發起一個http請求
        mockMvc.perform(MockMvcRequestBuilders.get("/hotel/detail?id=1")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .accept(MediaType.APPLICATION_JSON))
                // 對結果斷言
                .andExpect(MockMvcResultMatchers.jsonPath("$.roomNum").value(20))
                // 打印請求內容
                .andDo(MockMvcResultHandlers.print());
    }

}

測試業務層代碼service

通常我們在service中編寫邏輯實現,因此在進行單元測試的時候,需要考慮如下的條件:

  • 測試之前自動構造好數據,測試結束之后自動回滾數據構造
  • 將service依賴的service進行模擬打樁進來
  • 可能需要在數據庫中構造好數據

示例3:只執行service中的代碼邏輯,數據庫的查詢直接使用模擬的操作

@RunWith(MockitoJUnitRunner.class)
public class HotelServiceTest {

    @Spy
    @InjectMocks
    private HotelService hotelService = new HotelServiceImpl();
    @Mock
    private HotelMapper hotelMapper;

    @Before
    public void before(){
        Mockito.when(hotelMapper.addHotel(Mockito.any())).thenReturn(1);
    }
    
    @Test
    public void addHotel(){
        Hotel hotel = new Hotel();
        hotel.setId(5);
        hotel.setHotelName("雲山大酒店");
        hotel.setPrice(new BigDecimal("230"));
        hotel.setRoomNum(130);
        hotelService.addHotel(hotel);
    }

}

示例4:測試業務層代碼service,基於spring boot 測試框架進行單元測試(運行速度較慢)

這里通過測試驗證方法是否被調用,調用順序是否正確

@Rollback
@Transactional
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
public class HotelServiceTest2 {

    @MockBean
    private AccountService accountService;

    @Autowired
    private HotelService hotelService;

    @Test
    public void joinHotel(){

        Hotel hotel = new Hotel();
        hotel.setHotelName("大酒店");
        hotel.setPrice(new BigDecimal("230"));
        hotel.setRoomNum(130);

        // 設置任意參數均返回固定參數
        BDDMockito.given(this.accountService.findAccount(ArgumentMatchers.anyInt())).willReturn(null);

        Integer id = hotelService.joinHotel(hotel);
        Assert.assertNotNull(id);

        // 接口被調用測試統計
        Mockito.verify(accountService, Mockito.times(1))
                .findAccount(ArgumentMatchers.anyInt());

        // 檢查執行方法執行順序是否正確;inOrder(accountService)的參數可以多個,參數必須為mock出來的對象
        InOrder inOrder = Mockito.inOrder(accountService);
        inOrder.verify(accountService).findAccount(ArgumentMatchers.anyInt());
        inOrder.verify(accountService).addAccount(ArgumentMatchers.anyInt(), ArgumentMatchers.anyString());
    }
}

測試數據層Mapper

這個通常用的比較少,一般是復雜SQL語句時,為了快速的驗證是否正確,才會編寫的。

示例5:測試數據層Mapper,快速驗證編寫的SQL語句是否正確

@Rollback
@Transactional(rollbackFor = Exception.class)
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestDemoApplication.class)
public class HotelMapperTest {

    @Autowired
    private HotelMapper hotelMapper;

    /**
     * 對於@Sql 注解說明:可以提前執行一些SQL,比如下面要驗證SQL需要的數據
     */
    @Sql("/hotel.sql")
    @Test
    public void findHotel(){

        Hotel hotel = hotelMapper.selectById(100);
        System.out.println(hotel);
    }

}

靜態方法測試

當需要測試的方法中調用了靜態方法,但是我們不想讓靜態方法執行,此時需要使用powermock來對靜態方法進行mock。
需要將測試框架切換為@RunWith(PowerMockRunner.class),同時在@PrepareForTest注解中指定需要mock的靜態方法所屬的類;其他的操作都和原來的一樣。

示例6:靜態方法mock

@PrepareForTest({CacheUtil.class})// 可以配置多個,如果里面還依賴了其他靜態類,也需要這這里配置上
@RunWith(PowerMockRunner.class)
public class StaticMethodTest {

    @Spy
    @InjectMocks
    private final HotelService hotelService = new HotelServiceImpl();
    // @Mock
    // private HotelMapper hotelMapper;
    @Test
    public void findHotel(){
        Hotel hotel = new Hotel();
        hotel.setId(1);
        hotel.setHotelName("大酒店");
        hotel.setPrice(new BigDecimal("230"));
        hotel.setRoomNum(130);
    
        // Mockito.when(hotelMapper.selectById(1)).thenReturn(hotel);
    
        // 對CacheUtil的靜態方法着mock
        PowerMockito.mockStatic(CacheUtil.class);
        PowerMockito.when(CacheUtil.getVal(Mockito.any())).thenReturn(hotel);
        
        Hotel val = hotelService.findById(1);
        Assert.assertNotNull(val);
    }
}

其他注解和斷言語句

assertThat和Hamcrest

  • 單元測試結構
    • @Before
    • @Test
    • @After
  • 斷言
    • assertEquals
    • assertTrue / assertFalse
    • assertNull / assertNotNull
    • assertSame / assertNotSame
    • assertArrayEquals
    • assertThat
  • 測試異常
    • @Test(expected = NullPointException.class)
  • 主動失敗
    • fail
  • JUnit + Hamcrest
    • assertThat(str.indexOf("hello"), is(not(-1)))
    • assertThat(str.contains("hello"), equals(true))
    • assertThat(str, containsString("hello"))
    • is、not
    • equalTo / sameInstance、nullValue / notNullValue、instanceOf
    • hasProperty
    • hasEntry、hasKey、hasValue、hasItem / hasItems、hasItemInArray、in
    • greaterThan、greaterThanOrEqualTo、lessThan、lessThanOrEqualTo
    • containsString、endsWith、startsWith
  • JUnit + Mockito
    • when().thenReturn()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM