在一個項目開發中我們通常都是分工合作共同開發的,那么在業務中各個模塊可能會存在相互調用的情況。如果我們調用的某個模塊開發的同學還未開發完成,那么在進行單元測試的時候該如何辦呢?或者是我們只是想測試某個業務的邏輯代碼,不需要去連接那些基礎組件(比如數據庫這些)時,又應該如何做呢?再比如我們只想測試在某種情況下會自己的邏輯代碼是否正確,此時又該如何做呢?
當然你可能會想到直接去將相關的代碼寫死即可,但是萬一改動的地方比較多就很麻煩了;同時有的地方你改為死數據時,很可能待會兒你提交代碼時就會忘記,最后可能就會直接發布到正式環境里面去了。雖然直接寫死的方式效率很快,但是也容易發生錯誤;因此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()