<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 20038字,需瀏覽 41分鐘

           ·

          2021-05-02 10:22

          背景

          最近剛完成一個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,需要傳對象的地方都可以用
          • anyStringanyLong()、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,大家早安吧!

          - END -


          瀏覽 78
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线欧美区| 国产精品婷婷 | 精品久久久久久久久久大佬 | 成人无码电影333 | 操鼻视频素材大网站 |