• 肝到凌晨两点,不学习MOCK官方资料,直接上手的体验经历


    背景

    最近刚完成一个bug的修复,但是根据公司代码质量管理要求,所以改动代码必须编写测试用例,而且测试用例覆盖率必须达到50%,测试用例通过后,代码通过sonar扫描通过后(没有bug,单元测试他通过率大于]50%),方能将代码合入master分支,从这一点上讲,现在公司的代码管理确实规范多了,不像我之前待过的公司,测试、发布都是根据自己需要,想咋样都可以,代码质量压根就没管理。

    基于以上要求,我必须得自己写单元测试了,但之前确实没咋写过单元测试,对与Junit也仅仅停留在会用@Test注解,然后没了。所以单元测试这块一切都要重头学,但是为了效率我是没时间看教程的,只能照葫芦画瓢,照猫画虎,因此今天的内容全是我这两天直接实战踩坑的血泪史,不涉及官方文档和资料,需要说明的是,今天我们的单元测试是基于mockito实现的。

    踩坑过程

    为了尽可能接近我实战的环境,这里的业务代码都是伪代码,我们先创建springboot项目,同时引入mock的依赖。

    mock依赖

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>3.6.0</version>
    </dependency>
    

    MOCK的第一眼

    项目创建完成后,我们直接来看案例,我当时第一次看到别的单元测试是这样的:

    /**
     * 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);
        }
    }
    

    看完之后,我一脸懵逼,这都是啥东西?啥作用?干啥用?这是啥操作?满脸的黑人问号。之前看代码,单元测试根本就没关心过,想着不就是@Test吗,我也写过呀,别人写的单元测试和我没关系。但是昨天开始研究和琢磨以后,我裂开了,这都什么东东,很难受反正。

    不知道你看了上面的代码啥感觉,有没有和我第一次的感觉一样,上面的代码还是我简化之后的,如果你看到实际代码,可能会更崩溃,单词可能认识,注解没见过呀……反正是一次悲催,但还不错的体验,特别是顿悟之后的体验,不亚于解决了一个大bug

    关联代码

    下面是关联代码,所有的代码都是伪代码,业务逻辑和昨天实际可能差距比较大,但是说明问题足够了:

    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("企业保存成功");
            return 1;
        }
    
        public String selectEnterprise(Long id) {
            System.out.println("查询企业成功:" + 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("保存数据");
            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("企业不存在");
                return "企业不存在";
            }
            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("发送消息成功:" + 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注入的是你方法要用到的类

    但是知道了上面两点以后,我依然毫无进展,然后在我的无数次的坚持和摸索之下,我终于知道空指针的错误是因为依赖的类(就是项目中被@Autowired注入的类)要通过@Mock注入(别人告诉你的,在没理解,没形成认知,你思想上确实很难翻过那个梁),然后我把代码调整成这样:

    @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)
    

    然后,又琢磨来半天,查了好多博客,问题也没接近,最后请教同事,他也解决不了,使劲浑身解数也没有解决。所以问题又回到了我这里,我得自己解决问了,毕竟解决问题这种高光时刻还是要交给我来完成的,最后我也没有辜负问题的重托,完美解决了它。最后竟然是因为我制定的参数不够精确,你敢信,你敢信,你敢信……这也再一次告诉我们,代码不会有错,一定是你的问题,好好反思自己的问题。

    扩展知识

    这里要补充下mock的一些知识,主要涉及几个方法:

    • any():生成任意Object,需要传对象的地方都可以用
    • anyStringanyLong()anyInt()anyList()……:生成对应的类型

    上面这种方式,只针对可以为空的参数,类似于占位符,除了在given中调用方法外,在其他地方调用具体方法的时候,必须准确传值,否则会报如上错误

    调用成功了

    我把代码改成下面这也,单元测试通过了,也没报错:

    @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);
        }
    }
    

    为了应对覆盖率继续改进

    但是看了业务代码以后,我发现有部分业务没有跑,也就是单元测试未覆盖,如果要上线发布,那所有代码必须覆盖,所以我得想办法让业务继续往下走,这时候就是体现given方法价值的时候了,不过这都是后话,都是我经历了N次失败之后得出来的。

    昨天下班走的时候,我突然意识到,given方法不就相当于方法的拦截器吗,拦截方法,修改返回结果,那一刻我觉得我顿悟了,然后一切都豁然开朗了,比如这样的用法,其实就是修改了essageService.sendMessage的执行结果,把方法的返回值改成了user insert success

    given(messageService.sendMessage(anyString())).willReturn("user insert success");
    

    提示: 需要注意的是你需要将given方法mock的方法的调用参数全部改成any类型的,否则你修改的方法结果是不生效的,返回值结果会是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方式跟一下代码,你就会发现,代码中userService的值是null,这时候你只需要在单元测试中@Mock一下userService就可以啦。这里报错的原因是,因为之前业务逻辑没有触发,单元测试并没有运行这里的代码,所以自然也不需要注入相关依赖,但是后面我们修改了返回值之后,业务逻辑发生变化,这时候后面代码要被执行,但是业务逻辑依赖的类没有被注入,自然就报错了。只需要mock相关依赖,方法就可以执行。

    再次扩展

    关于@Mock我想补充一些内容,如果你只是mock了对应的类,那默认情况下该类所有实例方法的返回值都是null,但通常情况下,你为了满足一些特殊业务场景测试,需要定制返回值,那这时候given就显示出它的价值了,简单来说given就相当于方法的mock

    另外,还要补充一点——assert,中文名,断言,是Junit下的一个重要类,常用的方法有:assertEqualsassertFalseassertTrueassertNull等,简单来说就是对方法执行结果进行校验,以确保测试结果正确。

    总结

    其实,对于一个陌生事物,认知前和认知后,是一种很奇妙的感受,认知前你可能很难想明白,也想不通,哪怕别人告诉你答案,你也会困惑,因为你想不明白为什么;但是认知后,你又很难再回到再回到认知前那种呆萌状态,答案你就是在知道,但可能另一个人问你原因的时候,你可能也说不出来。这两种状态存在着某种临界点,你如果能够快速打破临界状态,那你的认知水平也会极大地提升。

    今天的内容,我其实特别想记录自己对mock单元测试的整个认知过程,但是我觉得我失败了,就像我上面说的那样,从已经有认知的点,回看当时自己未认知前的状态,很多当时困惑的细节已经丧失了,而且也想不明白当时为什么不知道,整个过程是不可逆的,很玄学。

    最后,想再说一点,其实学任何东西,都是实践出真知,就像今天这样,我在没有看官方文档,和相关教程的情况下,通过看代码,测试,还是对MOCK建立起了一些基础的认知,保证我可以很好地上手现在的工作,这样学习的好处在于,你的目标很明确,你就是要你的代码跑起来,虽然过程中会遇到很多问题,但你的目标始终不变。好了,今天就到这里吧,大家晚安。

    项目源码获取地址

    https://github.com/Syske/learning-dome-code/tree/dev/springboot-mock-demo
    

    昨天晚上肝到快两点,我太难了,刚刚醒来,睡眼惺忪,还看错表了,06:38看成了08:38,洗漱的时候,我还在纳闷闹钟为什么没响?洗完朦胧的睡眼,再看表,我擦,才06:42,心中一串串卧槽跑过。
    好吧,那就继肝吧,不过你别说,醒来直接去洗漱感觉还不错,瞬间感觉整个人有精神了,执行力杠杠的,后面就这样好好坚持吧,现在早上也不冷,很适合搞事情。OK,大家早安吧!

  • 相关阅读:
    读书笔记 ASP.NET 2.0编程珠玑
    为什么公司招聘一个好员工很难,程序员找份好工作也不容易
    读书笔记 ASP.NET 2.0高级编程 第31章 配置
    Win7 x64 旗舰版下重新注册IIS7.5
    T_SQL 开发的13个Tips
    报表服务扩展:基于WCF技术的报表服务扩展
    实现多国语言的Reporting Services项目
    技术人生:如何成为一位优秀的程序员
    幸福框架:待实现的基础应用列表
    技术人生:做人十心机
  • 原文地址:https://www.cnblogs.com/caoleiCoding/p/14716537.html
Copyright © 2020-2023  润新知