前言
上一篇講了 JUnit 5單元測(cè)試(二)—— 斷言,書(shū)接上文開(kāi)始 JUnit 5單元測(cè)試(三)—— Mockito 模擬
想象下面這幾種情況你該怎么單元測(cè)試:
1.A方法去數(shù)據(jù)庫(kù)查詢(xún)了數(shù)據(jù)進(jìn)行了一些處理,該怎么單元測(cè)試;
2.在微服務(wù)項(xiàng)目中,A方法中調(diào)用了遠(yuǎn)程微服務(wù)B方法(或者B方法還沒(méi)寫(xiě)好),該怎么單元測(cè)試;
3.A方法中從 redis 或者 Kafka 消息隊(duì)列里取了一些數(shù)據(jù)處理,該怎么單元測(cè)試;
可以看到上面幾種情況如果僅用斷言不能很好的支持單元測(cè)試,這時(shí)候就可以用 Mockito 來(lái)模擬數(shù)據(jù)進(jìn)行單元測(cè)試了。
一、什么是 Mockito
Mockito是一款開(kāi)源測(cè)試庫(kù),簡(jiǎn)稱(chēng) Mock , 該框架允許在自動(dòng)化或單元測(cè)試中模擬對(duì)象。簡(jiǎn)單來(lái)說(shuō)對(duì)于某些不容易構(gòu)造或者不容易獲取的比較復(fù)雜的數(shù)據(jù)/場(chǎng)景,模擬一個(gè)虛假的Mock對(duì)象來(lái)替代真實(shí)的對(duì)象。
想象一下這樣的情景:
一個(gè)用于和支付提供商(如 支付寶、某銀行)通信的 Java類(lèi),如果你測(cè)試時(shí)使用實(shí)時(shí)支付環(huán)境來(lái)對(duì)信用卡收費(fèi)相關(guān)代碼進(jìn)行測(cè)試是很危險(xiǎn)的,而且每次運(yùn)行單元測(cè)試時(shí)都需要實(shí)際連接到支付提供商,這會(huì)使單元測(cè)試具有不確定性,例如,如果支付提供商由于某種原因關(guān)閉了,那就不方便測(cè)試了。
如果你的測(cè)試數(shù)據(jù)依賴(lài)于外部系統(tǒng)、文件讀取時(shí)間過(guò)長(zhǎng)、數(shù)據(jù)庫(kù)連接不可靠,或者你不想在每次測(cè)試時(shí)發(fā)送電子郵件,那么 Mock 很有用。
Mock 一般用于以下情況的單元測(cè)試模擬數(shù)據(jù):
- 1.MVC接口驗(yàn)證,比如HTTP接口
- 2.數(shù)據(jù)庫(kù),做單元測(cè)試不需要連接數(shù)據(jù)庫(kù)
- 3.配置中心、網(wǎng)關(guān)等微服務(wù)發(fā)現(xiàn)治理依賴(lài)
- 4.Redis、zookeeper、mq消息隊(duì)列等第三方中間件
- 5.郵件、log、文件等服務(wù)
- 6.對(duì)其他服務(wù)有依賴(lài)的
二、引入依賴(lài)
<!-- 由于mockito 5 開(kāi)始支持的最低版本是jdk11,這里使用mockito 4的最新版本來(lái)支持jdk8及以上-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- 用于單元測(cè)試中使用@Mock注解時(shí)使用@ExtendWith(MockitoExtension.class)-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
三、創(chuàng)建 mock 實(shí)例
假設(shè)一個(gè)方法是查詢(xún)數(shù)據(jù)庫(kù)返回List集合,現(xiàn)在通過(guò)mock 來(lái)模擬返回的數(shù)據(jù),首先要?jiǎng)?chuàng)建 mock 實(shí)例來(lái)模擬數(shù)據(jù)。
創(chuàng)建 mock 實(shí)例有三種方法:調(diào)用靜態(tài) mock 方法、調(diào)用openMocks方法+@Mock 注解、Mockito擴(kuò)展+@Mock 注解
后文使用到的 Student 類(lèi)如下:
public class Student {
public String name;
public int id;
public Student(String name, int id) {
this.name = name;
this.id = id;
}
public String sayHello(String name) {
return "hello" + name;
}
public void setName(String name) {
this.name = name;
}
public String getName(String name,int id) {
return name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
1. 調(diào)用靜態(tài) mock 方法初始化 mock
在測(cè)試方法里使用 mock 靜態(tài)方法來(lái)模擬一個(gè)對(duì)象實(shí)例:
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import java.util.List;
import static org.mockito.Mockito.*;
public class MockTest {
@Test
public void whenNotUseMockAnnotation_thenCorrect() {
//使用靜態(tài) mock 方法來(lái)模擬一個(gè) List 對(duì)象
Student student = mock(Student.class);
//使用student做一些操作
//......
}
}
2. @Mock 注解初始化 mock
除了上面 mock 靜態(tài)方法來(lái)創(chuàng)建模擬對(duì)象實(shí)例,還可以使用 openMocks 來(lái)初始化 mock 然后使用 @Mock 注解來(lái)更簡(jiǎn)單的創(chuàng)建模擬對(duì)象實(shí)例。
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
import static org.mockito.Mockito.*;
public class MockTest {
@Mock
Student student; //使用 @Mock 注解來(lái)模擬一個(gè)student對(duì)象
@Test
public void whenNotUseMockAnnotation_thenCorrect() throws Exception {
//初始化Mock,(以前低版本的寫(xiě)法是使用initMocks(this)現(xiàn)高版本中改方法已被廢棄,轉(zhuǎn)而使用openMocks(this)初始化)
AutoCloseable closeable = MockitoAnnotations.openMocks(this);
//使用mockedList做一些操作
//......
//關(guān)閉mock
closeable.close();
}
}
可以把初始化 Mock 和關(guān)閉 mock 的代碼放到 @BeforeEach 和 @AfterEach 注解的方法中更合適,這樣如果你有多個(gè)測(cè)試方法就不必每個(gè)測(cè)試方法中都再寫(xiě)一遍初始化和關(guān)閉Mock的代碼了:
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
import static org.mockito.Mockito.*;
public class MockTest {
@Mock
Student student;
private AutoCloseable closeable;
@BeforeEach
void initService() {
closeable = MockitoAnnotations.openMocks(this);
}
@AfterEach
void closeService() throws Exception {
closeable.close();
}
@Test
public void whenNotUseMockAnnotation_thenCorrect() {
//使用mockedList做一些操作
//......
}
}
3. 使用Mockito JUnit 5 擴(kuò)展來(lái)初始化 mock
除了上面兩種方式,還有一個(gè)用于JUnit 5的 Mockito 擴(kuò)展庫(kù),它初始化 mock 更加簡(jiǎn)單,一般用這種方式用的比較多,下文的所有示例都將采用這種方式。
先添加如下 mock 擴(kuò)展依賴(lài):
<!-- 用于單元測(cè)試中使用@Mock注解時(shí)使用@ExtendWith(MockitoExtension.class)-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
在測(cè)試類(lèi)上面加上 @ExtendWith(MockitoExtension.class) 注解 ,然后使用 @Mock 注解修飾模擬對(duì)象即可:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class MockTest {
@Mock
Student student;
@Test
public void whenNotUseMockAnnotation_thenCorrect() {
//...
}
}
如果你測(cè)試類(lèi)里有多個(gè)測(cè)試方法,不想每個(gè)測(cè)試方法都共享模擬變量,還可以將模擬對(duì)象注入到方法參數(shù):
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class MockTest {
@Test
public void whenNotUseMockAnnotation_thenCorrect(@Mock Student student) {
//...
}
}
四、初始化mock后,mock對(duì)象會(huì)覆蓋掉整個(gè)被mock的對(duì)象
初始化mock后,mock對(duì)象會(huì)覆蓋掉整個(gè)被mock的對(duì)象,當(dāng)你直接調(diào)用mock實(shí)例對(duì)象的方法不會(huì)走真實(shí)的方法,只會(huì)返回默認(rèn)值(返回null或者空集合,或者0等基本類(lèi)型的值)。
舉個(gè)例子:
@Mock
Student student ;
@Mock
List<String> list ;
@Test
public void whenThenCorrect() {
student.setId(1);
System.out.println(student.getId()); //輸出0
list.add("a");
System.out.println(list.get(0)); //輸出null
System.out.println(list.size()); //輸出0
}
所以對(duì)于初始化之后的 mock 實(shí)例對(duì)象是不能直接調(diào)用其方法進(jìn)行返回東西的,要讓 mock 實(shí)例對(duì)象返回東西需要用 when…thenReturn 模擬方法返回值。
五、when…thenReturn 模擬方法返回值
(1) 對(duì)于有返回值的方法
when(mock.someMethod(arg1, arg2, …)).thenReturn(value)用于設(shè)置模擬的實(shí)例方法返回值,設(shè)置后再調(diào)用該方法不會(huì)運(yùn)行實(shí)例方法里的邏輯,將直接返回模擬的值:
@Mock
Student student;
@Test
public void whenThenCorrect() {
//設(shè)置 student.getName("張三",1)的返回值是"模擬的張三",后面的代碼如果調(diào)用 student.getName("張三",1)將直接返回"模擬的張三",不會(huì)去執(zhí)行 student.getName()里的邏輯
when(student.getName("張三",1)).thenReturn("模擬的張三");
//調(diào)用student.getName("張三",1)斷言為 "張三"
assertEquals("模擬的張三", student.getName("張三",1));
}
可以在 thenReturn(value1, value2, …) 里設(shè)置連續(xù)設(shè)定返回值,第一次調(diào)用時(shí)返回 value1,第二次返回value2:
@Mock
Student student;
@Test
public void whenThenCorrect() {
when(student.getName("張三",1)).thenReturn("張三","李四");
assertEquals("張三", student.getName("張三",1));
assertEquals("李四", student.getName("張三",1));
}
(2) 對(duì)于無(wú)返回值的方法
對(duì)于無(wú)返回值的方法使用 doNothing().when(mock).someMethod(arg1, arg2, …)來(lái)模擬
@Mock
Student student;
@Test
public void whenThenCorrect() {
doNothing().when(student).sayHello("張三");
student.sayHello("張三");
}
注意:
對(duì)于 static 、 final 、private修飾的方法和equals()、hashCode()方法, Mockito 無(wú)法對(duì)其進(jìn)行when(…).thenReturn(…) 操作。
六、參數(shù)化匹配器
@Mock
Student student;
@Test
public void whenThenCorrect() {
when(student.getName("張三",1)).thenReturn("模擬的張三");
assertEquals("模擬的張三", student.getName("張三",1));
}
在上面 when(mock.someMethod(arg1, arg2, …)).thenReturn(value) 里,我們所有的參數(shù) arg1、arg2 都是寫(xiě)死的,就像 student.getName(“張三”,1)這樣,這樣當(dāng)調(diào)用的時(shí)候也要寫(xiě)死了。
我們可以用參數(shù)化匹配器來(lái)優(yōu)化下:
(1) mockito 提供了很多參數(shù)匹配器
如:anyInt()、anyString()、anyDouble()、anyList()、anyMap()等
@Mock
Student student;
@Test
public void whenThenCorrect() {
//使用參數(shù)化匹配器 anyString()和 anyInt()
when(student.getName(anyString(),anyInt())).thenReturn("張三","李四");
//調(diào)用student.getName 隨便傳入兩個(gè)參數(shù),斷言為 "張三"
assertEquals("張三", student.getName("aa",12));
//調(diào)用student.getName 隨便傳入兩個(gè)參數(shù),斷言為 "李四"
assertEquals("李四", student.getName("bb",12));
}
(2) 使用參數(shù)匹配器時(shí),方法里所有參數(shù)都應(yīng)使用匹配器。
例如下面的寫(xiě)法就是錯(cuò)的:
when(student.getName(anyString(),1)).thenReturn("張三","李四");
如果要為參數(shù)使用特定值,則可以使用eq()方法:
@Mock
Student student;
@Test
public void whenThenCorrect() {
//使用參數(shù)化匹配器 anyString()和 anyInt()
when(student.getName(anyString(),eq(1))).thenReturn("張三","李四");
//調(diào)用student.getName 第一個(gè)參數(shù)隨便傳入,第二個(gè)參數(shù)要傳1。斷言為 "張三"
assertEquals("張三", student.getName("aa",1));
//調(diào)用student.getName 第一個(gè)參數(shù)隨便傳入,第二個(gè)參數(shù)要傳1。斷言為 "李四"
assertEquals("李四", student.getName("bb",1));
}
六、when…thenThrow 模擬異常拋出
使用 when(mock.someMethod()).thenThrow(Exception()) 模擬方法異常拋出
@Mock
Student student;
@Test
public void exceptionCorrect() {
//模擬當(dāng)調(diào)用 student.getName 時(shí)拋出 RuntimeException 異常
when(student.getName(anyString(),anyInt())).thenThrow(new RuntimeException());
//將拋出 RuntimeException 異常
student.getName("aa",1);
}
七、verify 驗(yàn)證方法是否被調(diào)用
有些時(shí)候,測(cè)試并不關(guān)心返回結(jié)果,而是關(guān)心方法是否被正確的參數(shù)調(diào)用過(guò),這時(shí)候就應(yīng)該使用驗(yàn)證方法了。
verify 用于驗(yàn)證模擬的實(shí)例方法是否被調(diào)用,若沒(méi)有調(diào)用則驗(yàn)證失敗,就報(bào)錯(cuò)提示:
@Mock
Student student;
@Test
public void verifyCorrect() {
when(student.getName(anyString(),anyInt())).thenReturn("張三");
assertEquals("張三", student.getName("aa",1));
// 驗(yàn)證模擬的 student 實(shí)例其 getName 方法是否被調(diào)用過(guò)
verify(student).getName(anyString(),anyInt());
}
verify 還可以使用 times 來(lái)驗(yàn)證方法調(diào)用的次數(shù),若實(shí)際調(diào)用次數(shù)和預(yù)期的不符合,就報(bào)錯(cuò)提示:
@Mock
Student student;
@Test
public void verifyCorrect() {
when(student.getName(anyString(),eq(1))).thenReturn("張三","李四");
assertEquals("張三", student.getName("aa",1));
assertEquals("李四", student.getName("bb",1));
//驗(yàn)證student.getName("aa",1)調(diào)用了1次
verify(student,times(1)).getName("aa",1);
//驗(yàn)證student.getName("bb",1)調(diào)用了1次
verify(student,times(1)).getName("bb",1);
//驗(yàn)證student.getName 總的調(diào)用了2次
verify(student,times(2)).getName(anyString(),eq(1));
}
八、Spy 運(yùn)行真實(shí)方法
有些時(shí)候我們不想對(duì)一個(gè)對(duì)象進(jìn)行 mock,但是我們想判斷一個(gè)普通對(duì)象的方法有沒(méi)有被調(diào)用過(guò),那你可以使用 Spy 來(lái)監(jiān)測(cè)對(duì)象,然后用 verify 來(lái)驗(yàn)證方法有沒(méi)有被調(diào)用。
(1)使用Spy方法
@Test
public void spyCorrect() {
//使用 spy 方法 監(jiān)測(cè) spyList
List<String> spyList = spy(new ArrayList<>());
spyList.add("one");
spyList.add("two");
//驗(yàn)證上面有沒(méi)有調(diào)用add("one")方法
verify(spyList).add("one");
assertEquals(2, spyList.size());
assertEquals("one", spyList.get(0));
assertEquals("two", spyList.get(1));
//size()和get(0)方法被模擬了返回值就不會(huì)去執(zhí)行其真實(shí)方法,get(1)沒(méi)被模擬會(huì)調(diào)用其真實(shí)方法返回值
when(spyList.size()).thenReturn(100);
when(spyList.get(0)).thenReturn("aa");
assertEquals(100, spyList.size());
assertEquals("aa", spyList.get(0));
assertEquals("two", spyList.get(1));
}
(2)使用 @Spy 注解
除了上面使用 Spy 方法,你也可以使用 @Spy 注解達(dá)到一樣的效果:
@Spy
List<String> spyList = new ArrayList<>();
@Test
public void spyCorrect() {
spyList.add("one");
spyList.add("two");
//驗(yàn)證上面有沒(méi)有調(diào)用add("one")方法
verify(spyList).add("one");
assertEquals(2, spyList.size());
assertEquals("one", spyList.get(0));
assertEquals("two", spyList.get(1));
//size()和get(0)方法被模擬了返回值就不會(huì)去執(zhí)行其真實(shí)方法,get(1)沒(méi)被模擬會(huì)調(diào)用其真實(shí)方法返回值
when(spyList.size()).thenReturn(100);
when(spyList.get(0)).thenReturn("aa");
assertEquals(100, spyList.size());
assertEquals("aa", spyList.get(0));
assertEquals("two", spyList.get(1));
}
九、@InjectMocks 注解解決依賴(lài)
上面第四點(diǎn)提到,初始化 mock 后,直接調(diào)用mock實(shí)例對(duì)象的方法不會(huì)走真實(shí)的方法,只會(huì)返回默認(rèn)值。
但是有些時(shí)候我們不想直接 mock 模擬對(duì)象,我們想實(shí)際的運(yùn)行對(duì)象的方法又讓它返回一個(gè)模擬值,但是這個(gè)對(duì)象的方法里又依賴(lài)了其他的對(duì)象。這個(gè)時(shí)候就可以使用 @InjectMocks 注解了。
@InjectMocks 創(chuàng)建一個(gè)類(lèi)的實(shí)例,并將使用 @Mock 注解創(chuàng)建的 mock 注入到這個(gè)實(shí)例中。
假設(shè)有 DatabaseDAO、NetworkDAO、MainClass 三個(gè)類(lèi),其中 MainClass 類(lèi)中的 save 方法需要用到 DatabaseDAO、NetworkDAO 。
public class DatabaseDAO {
public String save(String fileName) {
System.out.println("Saved in database");
return "Saved in database Ok";
}
}
public class NetworkDAO {
public String save(String fileName) {
System.out.println("Saved in network location");
return "Saved in network Ok";
}
}
public class MainClass {
DatabaseDAO database;
NetworkDAO network;
public boolean save(String fileName) {
String databaseResule = database.save(fileName);
System.out.println("Saved in database in Main class, "+databaseResule);
String netWorkResule = network.save(fileName);
System.out.println("Saved in network in Main class, "+netWorkResule);
return false;
}
}
單元測(cè)試:
@ExtendWith(MockitoExtension.class)
public class MainClassTest {
@InjectMocks
@Spy //加上@Spy 注解防止mock多線(xiàn)程運(yùn)行報(bào)錯(cuò)
MainClass mainClass;
@Mock
DatabaseDAO dependentClassOne;
@Mock
NetworkDAO dependentClassTwo;
@Test
public void injectMocksTest1() {
//不使用when..thenReturn模擬返回值,調(diào)用方法執(zhí)行后將返回真實(shí)返回值
assertFalse(mainClass.save("temp.txt"));
verify(mainClass).save("temp.txt");
}
@Test
public void injectMocksTest2() {
when(mainClass.save("temp.txt")).thenReturn(true);
//使用when..thenReturn模擬返回值,調(diào)用方法執(zhí)行后將返回模擬返回值
assertTrue(mainClass.save("temp.txt"));
verify(mainClass).save("temp.txt");
}
}
運(yùn)行此單元測(cè)試,輸出結(jié)果:
Saved in database in Main class, null
Saved in network in Main class, null
上面沒(méi)對(duì) dependentClassOne 和 dependentClassTwo 的 save 方法進(jìn)行返回值模擬,所以默認(rèn)返回了 null , 下面對(duì)他們模擬下返回值:
@ExtendWith(MockitoExtension.class)
public class MainClassTest {
@InjectMocks
@Spy //加上@Spy 注解防止mock多線(xiàn)程運(yùn)行報(bào)錯(cuò)
MainClass mainClass;
@Mock
DatabaseDAO dependentClassOne;
@Mock
NetworkDAO dependentClassTwo;
@Test
public void injectMocksTest2() {
when(dependentClassOne.save(anyString())).thenReturn("aa");
when(dependentClassTwo.save(anyString())).thenReturn("bb");
when(mainClass.save("temp.txt")).thenReturn(true);
assertTrue(mainClass.save("temp.txt"));
verify(mainClass).save("temp.txt");
}
}
運(yùn)行此單元測(cè)試,輸出結(jié)果:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-610043.html
Saved in database in Main class, aa
Saved in network in Main class, bb
參考:
Mockito Tutorial文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-610043.html
到了這里,關(guān)于JUnit 5單元測(cè)試(三)—— Mockito 模擬的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!