zoukankan      html  css  js  c++  java
  • JMockit使用总结

    Jmockit可以做什么

    使用JMockit API来mock被依赖的代码,从而进行隔离测试。

    • 类级别整体mock和部分方法重写
    • 实例级别整体mock和部分mock
    • mock静态方法、私有变量、局部方法
    • 灵活的参数匹配

    maven依赖

    Jmockit可以和junit和TestNG配合使用。需要注意的是:

    • 如果使用Junit4.5以上,jmockit依赖需要在junit4之前;或者在测试类上添加注解 @RunWith(JMockit.class)。
    • 如果是TestNG 6.2+ 或者 JUnit 5+, 没有位置限制
    		<!-- 如果使用Junit4.5以上 jmockit依赖需要在junit4之前 -->
    		<!-- 或者在测试类上添加注解 @RunWith(JMockit.class) -->
    		<!-- 如果是TestNG 6.2+ 或者 JUnit 5+, 没有位置限制 -->
    		<dependency>
    			<groupId>org.jmockit</groupId>
    			<artifactId>jmockit</artifactId>
    			<version>1.30</version>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>4.11</version>
    			<scope>test</scope>
    		</dependency>
    

    版本记录 版本更新较快。

    使用

    涉及到三个类:

    • 测试类:执行测试代码的类。
    • CUT(Code Under Test):被测试的类,测试此类是否能正确地工作。
    • 依赖类:CUT会调用依赖类的方法。

    CUT

    @Data
    public class Person {
    
        private String name;
        private Integer age;
        private Person friend;
    
        public Person(){}
    
        public Person(String name, Integer age, Person friend){
            this.age = age;
            this.name = name;
            this.friend = friend;
        }
    
        @Override
        public boolean equals(Object obj) {
            return this.name.equals(((Person)obj).getName());
        }
    }
    
    
    @Service
    public class PersonService {
    
        public String showName(String name){
            System.out.println("person show name : " + name);
            return name;
        }
    
        public int showAge(int age) {
            System.out.println("person show age : " + age);
            return age;
        }
    
        public Person getDefaultPerson(){
            return new Person("miao", 3, null);
        }
    }
    
    
    @Service
    public class CoderService {
        @Value("${coder.service.desc}")
        private String desc;
    
        public String showWork(String work){
            return work;
        }
    
        public int showSalary(int salary){
            return salary;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public String getPersonName(Person person){
            return person.getName();
        }
    }
    

    基本流程

    record(录制)---- replay(回放) ---- verify(验证)

    record : 设置将要被调用的方法和返回值。

    • Expections中的方法至少被调用一次,否则会出现missing invocation错误。调用次数和调用顺序不限。
    • StrictExpectations中方法调用的次数和顺序都必须严格执行。如果出现了在StrictExpectations中没有声明的方法,会出现unexpected invocation错误。

    replay:调用(未被)录制的方法,被录制的方法调用会被JMockit拦截并重定向到record阶段设定的行为。

    verify:基于行为的验证,测试CUT是否正确调用了依赖类,包括:调用了哪些方法;通过怎样的参数;调用了多少次;调用的相对顺序(VerificationsInOrder)等。可以使用times,minTimes,maxTimes来验证。

        @Test
        public void mockProcessTest(final @Mocked PersonService target){
            //录制预期行为
            new Expectations(){
                {
                    target.showName(anyString);
                    result = "test1";
                    target.showAge(anyInt);
                    result = -1;
                }
            };
    
            //测试代码
            Assert.assertTrue("test1".equals(target.showName("test2")));
            Assert.assertTrue(-1 == target.showAge(12));
            Assert.assertTrue(-1 == target.showAge(12));
    
            //验证
            new Verifications(){
                {
                    target.showName("test1");
                    times = 0; //执行了0次。参数一致的才会计数
                    target.showAge(12);
                    times = 2; //执行了2次
                }
            };
        }
    
        /**
         * Expections中的方法至少被调用一次,否则会出现missing invocation错误.
         * 调用次数和调用顺序不限.
         */
        @Test
        public void mockExpectationsProcessTest(final @Mocked PersonService service){
            new Expectations(){{
                service.showAge(anyInt);
                result = -1;
            }};
            //只调用showName会报错 Missing 1 invocation
            service.showName("hahah");
            service.showAge(12);
        }
        /**
         * StrictExpectations中方法调用的次数和顺序都必须严格执行。如果出现了在StrictExpectations中没有声明的方法,会出现unexpected invocation错误。
         * 没有必要做Verifications验证。
         */
        @Test
        public void mockStrictExpectationsProcessTest(final @Mocked PersonService service){
            new StrictExpectations(){{
                service.showAge(anyInt);
                result = -1;
                service.showName(anyString);
                result = "ok";
            }};
    
            //1.下面只执行了一个录制方法,报错:unexpected invocation, Missing invocation
           // Assert.assertTrue(-1 == service.showAge(12));
    
            //2.下面与录制顺序不一致,会报错:unexpected invocation, Missing invocation
           // Assert.assertTrue("ok".equals(service.showName("test")));
           // Assert.assertTrue(-1 == service.showAge(12));
    
            //3.调用没有录制的方法,报错 Unexpected invocation
           // service.getDefaultPerson();
    
            //必须全部执行录制的方法,且顺序一致
            Assert.assertTrue(-1 == service.showAge(12));
            Assert.assertTrue("ok".equals(service.showName("test")));
        }
    

    部分注解说明

    RunWith(JMockit.class): 指定单元测试的执行类为JMockit.class。
    Tested: 指定被测试类,同时mock实例并注入测试类;依赖的类使用Injectable注入。
    Injectable: 将对象进行mock并注入测试类。
    Mocked:mock一种类型,并注入测试类。

    Mocked与Injectable区别:

    • Mocked 注入的依赖,类的所有实例都被mock,record的方法,在replay时,按照record的结果返回;没有record的方法返回默认值。
    • Injectable 注入的依赖,只mock指定的实例,record的方法,在replay时,按照record的结果返回;没有record的方法返回默认值。没有mock的实例,调用其原始方法。
    @RunWith(JMockit.class)
    public class MockTest {
    
        //@Mocked 修饰,所有实例都会被mock
        @Mocked
        private PersonService personService;
    
        // @Injectable 修饰,只mock指定的实例。
        @Injectable
        private CoderService coderService;
    
        @Test
        public void testInstance(){
            new Expectations(){
                {
                    personService.showAge(anyInt);
                    result = -1;
    
                    personService.getDefaultPerson();
                    result = new Person("me", 4, null);
    
                    Deencapsulation.invoke(coderService, "showWork", anyString);
                    result = "java";
    
                }
            };
    
            //record的方法,按照给定的结果返回
            Assert.assertTrue(-1 == personService.showAge(11));
            Assert.assertTrue("java".equals(coderService.showWork("nothing")));
            Assert.assertTrue(4 == personService.getDefaultPerson().getAge());
            //没有录制的方法,返回默认值
            Assert.assertTrue(personService.showName("testName") == null);
            Assert.assertTrue(coderService.showSalary(100) == 0);
    
            //Mock 所有PersonServiceImpl实例
            PersonService pservice = new PersonService();
            Assert.assertTrue(-1 == pservice.showAge(11));
            Assert.assertTrue(pservice.showName("testName") == null);
    
            //新生成的CoderService实例没有被mock
            CoderService cservice = new CoderService();
            Assert.assertTrue("something".equals(cservice.showWork("something")));
            Assert.assertTrue(cservice.showSalary(100) == 100);
        }
    
        /**
         * 可以将参数注入,与类中注入结果一致。
         * 但是不要同时在参数中注入,且在测试类中注入,会影响执行结果。
         */
        @Test
        public void testInjectObj(final @Injectable CoderService coderService){
            new Expectations(){
                {
                    coderService.showWork(anyString);
                    result = "ok";
                }
            };
            Assert.assertTrue("ok".equals(coderService.showWork("hello")));
            Assert.assertTrue(coderService.showSalary(100) == 0);
        }
    
    

    使用示例

    • 部分mock(实例级别)
      在Expectations中传入被mock实例。 则replay的方法在Expectations中被录制时,按照record结果返回;没有被录制,则调用原有代码。
      与之对应的是Injectable 注入的实例,record的方法,在replay时按照record结果返回;没有record的方法,返回默认值。
        /**
         * 部分mock,在Expectations中传入被mock实例。
         * replay的方法在Expectations中被录制时,按照record结果返回;
         * 没有被录制,则调用原有代码
         */
        @Test
        public void partiallyMock(){
            new Expectations(personService){
                {
                    personService.showAge(anyInt);
                    result = -1;
                }
            };
            //被录制的方法,按照record结果返回
            Assert.assertTrue(-1 == personService.showAge(11));
            //未录制的方法,调用原有代码
            Assert.assertTrue("testName".equals(personService.showName("testName")));
        }
    
    • mockUp(类级别)
      mockUp的类,被mock的方法,replay的时候都执行mock的方法;没有被mock的方法,调用原有代码。
      与之对应的事Mocked注入的类,所有record的方法按照record结果返回;没有record的方法,返回默认值。
        /**
         * mockUp类,被mock的方法,replay的时候都执行mock的方法;
         * 没有被mock的方法,调用原有代码
         */
        @Test
        public void mockUpTest(){
            
            new MockUp<PersonService>(){
                @Mock
                public String showName(String name){
                    return "mocked";
                }
            };
    
            Assert.assertTrue("mocked".equals(new PersonService().showName("test")));
            Assert.assertTrue(1 == new PersonService().showAge(1));
        }
    
    • mock 静态方法
        @Test
        public void testStaticMethod(){
            new Expectations(CollectionUtils.class){{
                CollectionUtils.isEmpty((Collection<?>) any);
                result = true;
            }};
            List<Integer> list = Lists.newArrayList(1,2,3);
            Assert.assertTrue(list.size() == 3);
            Assert.assertTrue(CollectionUtils.isEmpty(list));
        }
    
    • mock 私有变量
    • 局部方法
    • mock 参数匹配问题
      参数为基本类型时,若mock方法参数设置为anyXXX,则任意此类型参数都可mock成功;若mock方法参数为具体值,则实际参数 equals mock参数时,才能mock成功。
      参数为非基本类型时,mock参数不可以为any,执行报错;若mock参数为具体值,只有传递的参数 equals mock参数时,才能mock成功。
    public class CoderServiceTest {
    
        @Tested
        private CoderService coderService;
        @Injectable
        private PersonService personService;
    
        @Test
        public void testMockCase(){
            new Expectations(coderService){{
                //mock私有变量
                Deencapsulation.setField(coderService, "desc", "coderDesc");
                //mock 方法
                Deencapsulation.invoke(coderService, "showWork", anyString);
                result = "noWork";
            }};
    
            //mock 私有变量成功
            Assert.assertTrue(coderService.getDesc().equals("coderDesc"));
            //mock 私有方法
            Assert.assertTrue(coderService.showWork("coder").equals("noWork"));
        }
        
        @Test
        public void testParamCase(){
            new Expectations(coderService){{
                //基本类型,mock参数为anyXXX
                Deencapsulation.invoke(coderService, "showWork", anyString);
                result = "mocked";
    
                //基本类型,mock参数为实际值
                Deencapsulation.invoke(coderService, "showSalary", 12);
                result = -1;
    
                //非基本类型,mock参数不可以为anyXXX,会报错 java.lang.IllegalArgumentException: Invalid null value passed as argument 0
            //    Deencapsulation.invoke(coderService, "getPersonName", (Person)any);
            //    result = "mocked";
    
                //基本类型,mock参数为实际值
                Deencapsulation.invoke(coderService, "getPersonName", new Person("me", 3, null));
                result = "mocked";
            }};
    
            //基本类型,mock参数为anyXXX, 实际参数为任意值mock成功
            Assert.assertTrue(coderService.showWork("java").equals("mocked"));
    
            //基本类型,mock参数为具体值, 实际参数 equals mock参数时,mock成功
            Assert.assertTrue(coderService.showSalary(12) == -1);
            Assert.assertTrue(coderService.showSalary(100) == 100);
    
            //基本类型,mock参数为实际值,实际参数 equals mock参数时,mock成功
            Assert.assertTrue("mocked".equals(coderService.getPersonName(new Person("me", 4, null))));
            Assert.assertFalse("mocked".equals(coderService.getPersonName(new Person("you", 3, null))));
        }
    
    }
    

    注意事项

    • Tested指定的被测试类,必须是实现类,而非接口。否则不能正确实例化,报错NPE。

    参考

    JMockit git book 很挫!!!
    JMockit Tutorial 很详细,部分示例已过时,推荐指数四颗星
    官网 英文版
    JMockit mock 实例使用的是asm技术

  • 相关阅读:
    ssh无密码登录设置方法以及出现问题 ECDSA host key 和IP地址对应的key不同的解决
    asp.net core 3 Swagger 添加 Authorization [Bearer token]
    asp.net core3 发布到IIS
    asp.net core 3.1 使用log4net
    asp.net core 3 跨域
    解决'vue' 不是内部或外部命令,也不是可运行的程序 或批处理文件的方法
    ReSharper 安装没有提示功能
    Python3 安装MySQL驱动
    win10 安装 Python 和 pip 详解
    Win10下 80端口被system(pid=4)占用的解决方法
  • 原文地址:https://www.cnblogs.com/shoren/p/jmokit-summary.html
Copyright © 2011-2022 走看看