本文將按照如下順序給大家簡單講講單元測試應該怎麼寫
單元測試又稱模組測試,是針對軟體設計的最小單位(模組)就行正確性的校驗的測試,檢查每個程式模組是否實現了規定的功能,保證其正常工作。
測試的重點:系統模組、方法的邏輯正確性
和整合測試不同,單元測試應該具備如下特點:
我們在企業開發中,很多大公司都是要求單測到達一定的比率才能提交程式碼,單測能夠保證我們寫的邏輯程式碼符合我們的預期,並且在後續的維護中都能通過單測來驗證我們的修改有沒有把原有的程式碼邏輯改錯。
雖然會花費我們額外10%的時間去做單測,但是收益率還是值得的,作為一個開發,我認為我們本就該進行完整的自測後才移交給測試同學。
先寫一個簡單的單測例子:測試一個求兩個set集合交集的方法
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
/**
* 獲取交集
* @param set1
* @param set2
* @return
*/
public Set<Integer> getIntersection(Set<Integer> set1,Set<Integer> set2){
set1.retainAll(set2);
return set2;
}
我們可以通過IDEA的自動生成功能來生成測試方法
它會在test目錄下的同包名下生成一個測試類
class HelloServiceTest {
@Test
void getIntersection() {
//生成mock類
HelloService helloService = Mockito.mock(HelloService.class);
//呼叫mock類的getIntersection方法時呼叫真實方法
Mockito.when(helloService.getIntersection(Mockito.anySet(),Mockito.anySet())).thenCallRealMethod();
Set<Integer> set1=new HashSet<>();
set1.add(1);
set1.add(2);
set1.add(3);
Set<Integer> set2=new HashSet<>();
set2.add(5);
set2.add(4);
set2.add(3);
Set<Integer> intersection = helloService.getIntersection(set1, set2);
Set<Integer> set3=new HashSet<>();
set3.add(3);
//斷言,判斷方法結果是否和我們預想的一致
Assertions.assertEquals(intersection,set3);
}
}
執行結果:
執行完後發現斷言異常,這樣就能檢查出我們之前寫的程式碼不對,去檢查了下,發現了問題,改正程式碼後重試。
public Set<Integer> getIntersection(Set<Integer> set1,Set<Integer> set2){
set1.retainAll(set2);
return set1;
}
HelloService helloService = Mockito.mock(HelloService.class);
@Mock
private HelloService helloService;
@Test
void getIntersection() {
//使用@Mock,需要加下面這行程式碼
MockitoAnnotations.openMocks(this);
Mockito.when(helloService.getIntersection(Mockito.anySet(),Mockito.anySet())).thenCallRealMethod();
...
}
mock出來的物件,要指定方法的返回,否則只是返回預設值,不會執行真正的方法的實現。
HelloService helloService = new HelloService();
@Spy
private HelloService helloService;
使用@Spy註解的物件,在執行的時候會呼叫真實的方法。
上面都是簡單的一級物件的構建,如果被測試的物件裡面還要物件依賴怎麼辦呢?
構建依賴的測試物件
如這個方法:
@Setter
public class HelloService {
private HelloDao helloDao;
public String hello(){
return helloDao.hello()+" xiaowang";
}
}
HelloService helloService=new HelloService();
HelloDao helloDao = Mockito.mock(HelloDao.class);
helloService.setHelloDao(helloDao);
使用@InjectMocks可以將mock出的依賴物件注入到它標註的測試物件中
@InjectMocks
private HelloService helloService;
@Mock
private HelloDao helloDao;
上面的例子中,將helloDao注入到了helloService中
構建靜態物件
需要修改依賴
<!-- <dependency>-->
<!-- <groupId>org.mockito</groupId>-->
<!-- <artifactId>mockito-core</artifactId>-->
<!-- <version>4.3.1</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.3.1</version>
<scope>test</scope>
</dependency>
MockedStatic<JsonUtils> tMockedStatic = Mockito.mockStatic(JsonUtils.class);
因為靜態方法mock了之後,在整個執行緒中都是生效的,如果需要隔離的話,可以使用try-with-resources來寫。
區別如下:
接下來我們學習方法的行為規定,因為mock出來的物件預設是不執行真實方法的,需要我們指定。
Mockito.doReturn("hello").when(helloDao).hello();
Mockito.when(helloDao.hello()).thenReturn("hello");
這種方式可以靈活的返回,比如根據引數的不同返回不同的值
Mockito.when(helloDao.hello(Mockito.anyString())).thenAnswer( invocation->{
String param = invocation.getArgument(0);
if(param.equals("w")){
return "wang";
}else {
return "li";
}
});
有時候需要測試方法異常的時候對整個方法體的影響
Mockito.when(helloDao.hello(Mockito.anyString())).thenThrow(NullPointerException.class);
我們執行完測試方法後,就需要對結果進行驗證比對,來證明我們的方法的正確性。
Assertions.assertEquals(hello,"hello xiaowang");
Assertions.assertTrue(hello.equals("hello xiaowang"));
異常斷言,判斷是否是預期的異常
Assertions.assertThrows(NullPointerException.class,()->{
helloDao.hello();
});
Mockito.verify(helloDao,Mockito.times(1)).hello();
另外還有兩個註解,@BeforeEach和@AfterEach,顧名思義,一個是在test方法執行前執行,一個是在test方法執行後執行。
@BeforeEach
public void before(){
System.out.println("before");
}
@AfterEach
public void after(){
System.out.println("after");
}
另外推薦兩款比較好用的單測生成外掛 TestMe 和Diffblue