不看MOCK官方資料直接開肝的慘痛經(jīng)歷……

背景
最近剛完成一個bug的修復(fù),但是根據(jù)公司代碼質(zhì)量管理要求,所以改動代碼必須編寫測試用例,而且測試用例覆蓋率必須達到50%,測試用例通過后,代碼通過sonar掃描通過后(沒有bug,單元測試他通過率大于]50%),方能將代碼合入master分支,從這一點上講,現(xiàn)在公司的代碼管理確實規(guī)范多了,不像我之前待過的公司,測試、發(fā)布都是根據(jù)自己需要,想咋樣都可以,代碼質(zhì)量壓根就沒管理
。
基于以上要求,我必須得自己寫單元測試了,但之前確實沒咋寫過單元測試,對與Junit也僅僅停留在會用@Test注解,然后沒了。所以單元測試這塊一切都要重頭學(xué),但是為了效率我是沒時間看教程的,只能照葫蘆畫瓢,照貓畫虎,因此今天的內(nèi)容全是我這兩天直接實戰(zhàn)踩坑的血淚史,不涉及官方文檔和資料,需要說明的是,今天我們的單元測試是基于mockito實現(xiàn)的。
踩坑過程
為了盡可能接近我實戰(zhàn)的環(huán)境,這里的業(yè)務(wù)代碼都是偽代碼,我們先創(chuàng)建springboot項目,同時引入mock的依賴。
mock依賴
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.6.0</version>
</dependency>
MOCK的第一眼
項目創(chuàng)建完成后,我們直接來看案例,我當時第一次看到別的單元測試是這樣的:
/**
* test
*
* @author syske
* @version 1.0
* @date 2021-04-28 下午10:14
*/
@RunWith(MockitoJUnitRunner.class)
public class UserServiceServiceTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserMapper userMapper;
@Mock
private MessageServiceImpl messageService;
@Test
public void saveUserTest1() {
String userId = "test2312";
given(userMapper.selectUser(anyString())).willReturn("admin");
int saveUser1 = userService.saveUser(userId);
assertEquals(saveUser1, -1);
}
@Test
public void saveUserTest2() {
String userId = "test2312";
given(userMapper.intsertUser(anyString())).willReturn(2);
given(messageService.sendMessage(anyString())).willReturn("user insert success");
int saveUser2 = userService.saveUser(userId);
assertEquals(saveUser2, 4);
}
}
看完之后,我一臉懵逼,這都是啥東西?啥作用?干啥用?這是啥操作?滿臉的黑人問號
。之前看代碼,單元測試根本就沒關(guān)心過,想著不就是@Test嗎,我也寫過呀,別人寫的單元測試和我沒關(guān)系。但是昨天開始研究和琢磨以后,我裂開了
,這都什么東東,很難受反正。
不知道你看了上面的代碼啥感覺,有沒有和我第一次的感覺一樣,上面的代碼還是我簡化之后的,如果你看到實際代碼,可能會更崩潰,單詞可能認識,注解沒見過呀……反正是一次悲催,但還不錯的體驗,特別是頓悟之后的體驗,不亞于解決了一個大bug。
關(guān)聯(lián)代碼
下面是關(guān)聯(lián)代碼,所有的代碼都是偽代碼,業(yè)務(wù)邏輯和昨天實際可能差距比較大,但是說明問題足夠了:
Mapper
enterprise
/**
* enterprise
*
* @author syske
* @version 1.0
* @date 2021-04-28 下午9:57
*/
@Component
public class EnterpriseMapper {
@Autowired
private MessageServiceImpl messageService;
public int insertEnterprise(Long id) {
System.out.println("保存enterprise:" + id);
messageService.sendMessage("企業(yè)保存成功");
return 1;
}
public String selectEnterprise(Long id) {
System.out.println("查詢企業(yè)成功:" + id);
return "" + id;
}
}
message
/**
* mapper
*
* @author syske
* @version 1.0
* @date 2021-04-27 下午11:34
*/
@Component
public class MessageMapper {
public List<String> listStrs(Long id) {
return new ArrayList();
}
public String insert(String data) {
System.out.println("保存數(shù)據(jù)");
return data;
}
}
UserMapper
/**
* user
*
* @author syske
* @version 1.0
* @date 2021-04-28 下午10:01
*/
@Component
public class UserMapper {
public int intsertUser(String userId) {
System.out.println("保存用戶:" + userId);
return 1;
}
public String selectUser(String userId) {
System.out.println("查詢用戶:" + userId);
return userId;
}
}
Serive
EnterpriseServiceImpl
/**
* Mockservice
*
* @author syske
* @version 1.0
* @date 2021-04-27 下午11:29
*/
@Service
public class EnterpriseServiceImpl {
@Autowired
private EnterpriseMapper enterpriseMapper;
@Autowired
private UserServiceImpl userService;
public String saveEnterpriseData(Long id, String userId, List<String> strs) {
String enterprise = enterpriseMapper.selectEnterprise(id);
if (!"admin".equals(enterprise)) {
System.out.println("企業(yè)不存在");
return "企業(yè)不存在";
}
int insertEnterprise = enterpriseMapper.insertEnterprise(id);
int saveUser = userService.saveUser(userId);
return "hello" + insertEnterprise + saveUser + strs;
}
}
MessageServiceImpl
/**
* mock2
*
* @author syske
* @version 1.0
* @date 2021-04-28 下午9:51
*/
@Service
public class MessageServiceImpl {
@Autowired
private MessageMapper messageMapper;
@Autowired
private EnterpriseServiceImpl messageService;
@Autowired
private EnterpriseMapper enterpriseMapper;
public String sendMessage(String message) {
messageMapper.insert(message);
return "success";
}
}
UserServiceImpl
/**
* user service
*
* @author syske
* @version 1.0
* @date 2021-04-28 下午10:04
*/
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
@Autowired
private MessageServiceImpl messageService;
public int saveUser(String userId) {
if ("admin".equals(userMapper.selectUser(userId))) {
System.out.println("用戶已存在");
return -1;
}
int i = userMapper.intsertUser(userId);
String sendMessage = messageService.sendMessage("用戶保存成功");
System.out.println("發(fā)送消息成功:" + sendMessage);
return 2 + i;
}
}
開始踩坑
第一次嘗試
我參照第一眼的mock單元測試,寫了自己人生中的第一個Mock單元測試,它大概長這樣:
/**
* unit test
*
* @author syske
* @version 1.0
* @date 2021-04-27 下午11:13
*/
@RunWith(MockitoJUnitRunner.class)
public class EnterpriseServiceTest {
@InjectMocks
private EnterpriseServiceImpl enterpriseService;
@Test
public void test() {
ArrayList<String> ls = new ArrayList<>();
ls.add("sdfsdf");
enterpriseService.saveEnterpriseData(any(), any(), any());
}
}
但很不幸的是,第一步我就失敗了(出師未捷身先死,太難了),紅色的提示告訴我問題沒這么難,不就是空指針嗎:

第N次嘗試
搗鼓了半天,事實告訴我問題沒這么簡單
,請教了身邊的同事,他告訴我兩點:
@InjectMocks注入的是要測試的方法所屬的類@Mock注入的是你方法要用到的類
但是知道了上面兩點以后,我依然毫無進展,然后在我的無數(shù)次的堅持和摸索之下,我終于知道空指針的錯誤是因為依賴的類(就是項目中被@Autowired注入的類)要通過@Mock注入(別人告訴你的,在沒理解,沒形成認知,你思想上確實很難翻過那個梁),然后我把代碼調(diào)整成這樣:
@RunWith(MockitoJUnitRunner.class)
public class EnterpriseServiceTest {
@InjectMocks
private EnterpriseServiceImpl enterpriseService;
@Mock
private EnterpriseMapper enterpriseMapper;
@Test
public void test() {
ArrayList<String> ls = new ArrayList<>();
ls.add("sdfsdf");
enterpriseService.saveEnterpriseData(any(), any(), any());
}
}
再次失敗
這時候錯誤變了,變成這樣的提示了:
org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
1 matchers expected, 3 recorded:
-> at io.github.syske.springbootmockdemo.EnterpriseServiceTest.test(EnterpriseServiceTest.java:38)
-> at io.github.syske.springbootmockdemo.EnterpriseServiceTest.test(EnterpriseServiceTest.java:38)
-> at io.github.syske.springbootmockdemo.EnterpriseServiceTest.test(EnterpriseServiceTest.java:38)
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(anyObject(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(anyObject(), eq("String by matcher"));
For more info see javadoc for Matchers class.
at io.github.syske.springbootmockdemo.service.EnterpriseServiceImpl.saveEnterpriseData(EnterpriseServiceImpl.java:28)
at io.github.syske.springbootmockdemo.EnterpriseServiceTest.test(EnterpriseServiceTest.java:38)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:54)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:99)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:105)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:40)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
然后,又琢磨來半天,查了好多博客,問題也沒接近,最后請教同事,他也解決不了,使勁渾身解數(shù)也沒有解決。所以問題又回到了我這里,我得自己解決問了,畢竟解決問題這種高光時刻還是要交給我來完成的,最后我也沒有辜負問題的重托,完美解決了它
。最后竟然是因為我制定的參數(shù)不夠精確,你敢信
,你敢信
,你敢信
……這也再一次告訴我們,代碼不會有錯,一定是你的問題,好好反思自己的問題
。
擴展知識
這里要補充下mock的一些知識,主要涉及幾個方法:
any():生成任意Object,需要傳對象的地方都可以用anyString、anyLong()、anyInt()、anyList()……:生成對應(yīng)的類型
上面這種方式,只針對可以為空的參數(shù),類似于占位符,除了在given中調(diào)用方法外,在其他地方調(diào)用具體方法的時候,必須準確傳值,否則會報如上錯誤
調(diào)用成功了
我把代碼改成下面這也,單元測試通過了,也沒報錯:
@RunWith(MockitoJUnitRunner.class)
public class EnterpriseServiceTest {
@InjectMocks
private EnterpriseServiceImpl enterpriseService;
@Mock
private EnterpriseMapper enterpriseMapper;
@Test
public void test() {
ArrayList<String> ls = new ArrayList<>();
ls.add("sdfsdf");
enterpriseService.saveEnterpriseData(12323L, "testets", ls);
}
}
為了應(yīng)對覆蓋率繼續(xù)改進
但是看了業(yè)務(wù)代碼以后,我發(fā)現(xiàn)有部分業(yè)務(wù)沒有跑,也就是單元測試未覆蓋,如果要上線發(fā)布,那所有代碼必須覆蓋,所以我得想辦法讓業(yè)務(wù)繼續(xù)往下走,這時候就是體現(xiàn)given方法價值的時候了,不過這都是后話,都是我經(jīng)歷了N次失敗之后得出來的。
昨天下班走的時候,我突然意識到,given方法不就相當于方法的攔截器嗎,攔截方法,修改返回結(jié)果,那一刻我覺得我頓悟了,然后一切都豁然開朗了,比如這樣的用法,其實就是修改了essageService.sendMessage的執(zhí)行結(jié)果,把方法的返回值改成了user insert success:
given(messageService.sendMessage(anyString())).willReturn("user insert success");
提示: 需要注意的是你需要將given方法mock的方法的調(diào)用參數(shù)全部改成any類型的,否則你修改的方法結(jié)果是不生效的,返回值結(jié)果會是NUll:
given(enterpriseMapper.selectEnterprise(12323L)).willReturn("admin");
但是這樣寫的話,返回值就是你willReturn指定的值:
given(enterpriseMapper.selectEnterprise(anyLong())).willReturn("admin");
另外,還有一點要注意的是,willReturn指定的值類似必須和方法的返回值類型一致,否則會報編譯錯誤。
加了given處理代碼之后,單元測試就可以保證全覆蓋了,但是不巧的是,這時候竟然報錯了:
java.lang.NullPointerException
at io.github.syske.springbootmockdemo.service.EnterpriseServiceImpl.saveEnterpriseData(EnterpriseServiceImpl.java:34)
at io.github.syske.springbootmockdemo.EnterpriseServiceTest.test(EnterpriseServiceTest.java:39)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:54)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:99)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:105)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:40)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
如果你debug方式跟一下代碼,你就會發(fā)現(xiàn),代碼中userService的值是null,這時候你只需要在單元測試中@Mock一下userService就可以啦。這里報錯的原因是,因為之前業(yè)務(wù)邏輯沒有觸發(fā),單元測試并沒有運行這里的代碼,所以自然也不需要注入相關(guān)依賴,但是后面我們修改了返回值之后,業(yè)務(wù)邏輯發(fā)生變化,這時候后面代碼要被執(zhí)行,但是業(yè)務(wù)邏輯依賴的類沒有被注入,自然就報錯了。只需要mock相關(guān)依賴,方法就可以執(zhí)行。
再次擴展
關(guān)于@Mock我想補充一些內(nèi)容,如果你只是mock了對應(yīng)的類,那默認情況下該類所有實例方法的返回值都是null,但通常情況下,你為了滿足一些特殊業(yè)務(wù)場景測試,需要定制返回值,那這時候given就顯示出它的價值了,簡單來說given就相當于方法的mock。
另外,還要補充一點——assert,中文名,斷言,是Junit下的一個重要類,常用的方法有:assertEquals、assertFalse、assertTrue、assertNull等,簡單來說就是對方法執(zhí)行結(jié)果進行校驗,以確保測試結(jié)果正確。
總結(jié)
其實,對于一個陌生事物,認知前和認知后,是一種很奇妙的感受,認知前你可能很難想明白,也想不通,哪怕別人告訴你答案,你也會困惑,因為你想不明白為什么;但是認知后,你又很難再回到再回到認知前那種呆萌狀態(tài),答案你就是在知道,但可能另一個人問你原因的時候,你可能也說不出來。這兩種狀態(tài)存在著某種臨界點,你如果能夠快速打破臨界狀態(tài),那你的認知水平也會極大地提升。
今天的內(nèi)容,我其實特別想記錄自己對mock單元測試的整個認知過程,但是我覺得我失敗了,就像我上面說的那樣,從已經(jīng)有認知的點,回看當時自己未認知前的狀態(tài),很多當時困惑的細節(jié)已經(jīng)喪失了,而且也想不明白當時為什么不知道,整個過程是不可逆的,很玄學(xué)。
最后,想再說一點,其實學(xué)任何東西,都是實踐出真知,就像今天這樣,我在沒有看官方文檔,和相關(guān)教程的情況下,通過看代碼,測試,還是對MOCK建立起了一些基礎(chǔ)的認知,保證我可以很好地上手現(xiàn)在的工作,這樣學(xué)習(xí)的好處在于,你的目標很明確,你就是要你的代碼跑起來,雖然過程中會遇到很多問題,但你的目標始終不變。好了,今天就到這里吧,大家晚安
。
項目源碼獲取地址
https://github.com/Syske/learning-dome-code/tree/dev/springboot-mock-demo
昨天晚上肝到快兩點,我太難了
,剛剛醒來,睡眼惺忪,還看錯表了,06:38看成了08:38,洗漱的時候,我還在納悶鬧鐘為什么沒響?洗完朦朧的睡眼,再看表,我擦
,才06:42,心中一串串臥槽跑過。
好吧,那就繼肝吧,不過你別說,醒來直接去洗漱感覺還不錯,瞬間感覺整個人有精神了,執(zhí)行力杠杠的,后面就這樣好好堅持吧,現(xiàn)在早上也不冷,很適合搞事情。OK,大家早安吧!
