zoukankan      html  css  js  c++  java
  • mock测试及jacoco覆盖率

    单元测试是保证项目代码质量的有力武器,但是有些业务场景,依赖的第三方没有测试环境,这时候该怎么做Unit Test呢,总不能直接生产环境硬来吧?

    可以借助一些mock测试工具来解决这个难题(比如下面要讲的mockito),废话不多说,直奔主题:

    一、准备示例Demo

    假设有一个订单系统,用户可以创建订单,同时下单后要检测用户余额(如果余额不足,提醒用户充值),具体来说,里面有2个服务:OrderService、UserService,类图如下:

     示例代码:

    package com.cnblogs.yjmyzz.springbootdemo.service.impl;
    
    import com.cnblogs.yjmyzz.springbootdemo.service.UserService;
    import org.springframework.stereotype.Service;
    
    import java.math.BigDecimal;
    
    /**
     * @author 菩提树下的杨过
     */
    @Service("userService")
    public class UserServiceImpl implements UserService {
    
    
        @Override
        public BigDecimal queryBalance(int userId) {
            System.out.println("queryBalance=>userId:" + userId);
            //模拟返回100元余额
            return new BigDecimal(100);
        }
    }
    

    package com.cnblogs.yjmyzz.springbootdemo.service.impl;
    
    import com.cnblogs.yjmyzz.springbootdemo.service.OrderService;
    import com.cnblogs.yjmyzz.springbootdemo.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import java.math.BigDecimal;
    
    @Service("orderService")
    public class OrderServiceImpl implements OrderService {
    
        @Autowired
        private UserService userService;
    
        /**
         * 下订单
         *
         * @param productName
         * @param orderNum
         * @return
         * @throws Exception
         */
        @Override
        public Long createOrder(String productName, Integer orderNum, int userId) throws Exception {
            System.out.println("createOrder=>userId:" + userId);
            if (StringUtils.isEmpty(productName)) {
                throw new Exception("productName is empty");
            }
    
            if (orderNum == null) {
                throw new Exception("orderNum is null!");
            }
    
            if (orderNum <= 0) {
                throw new Exception("orderNum must bigger than 0");
            }
    
            //下订单过程略,返回1L做为订单号
            Long orderId = 1L;
    
            //模拟检测余额
            BigDecimal balance = userService.queryBalance(userId);
            if (balance.compareTo(BigDecimal.TEN) <= 0) {
                System.out.println("余额不足10元,请及时充值!");
            }
    
            return orderId;
        }
    }
    

    里面的逻辑不是重点,随便看看就好。关注下createOrder方法,最后几行OrderService调用了UserService查询余额,即:OrderService依赖UserService,假设UserService就是一个第3方服务,不具备测试环境,本文就来讲讲如何对UserService进行mock测试。

    二、pom引入mockito 及 jacoco plugin

    2.1 引入mockito

    1 <dependency>
    2     <groupId>org.mockito</groupId>
    3     <artifactId>mockito-all</artifactId>
    4     <version>1.9.5</version>
    5     <scope>test</scope>
    6 </dependency>
    View Code

    mockito是一个mock工具库,马上会讲到用法。

    2.2 引入jacoco插件

     1 <plugin>
     2     <groupId>org.jacoco</groupId>
     3     <artifactId>jacoco-maven-plugin</artifactId>
     4     <version>0.8.5</version>
     5     <executions>
     6         <execution>
     7             <id>prepare-agent</id>
     8             <goals>
     9                 <goal>prepare-agent</goal>
    10             </goals>
    11         </execution>
    12         <execution>
    13             <id>report</id>
    14             <phase>prepare-package</phase>
    15             <goals>
    16                 <goal>report</goal>
    17             </goals>
    18         </execution>
    19         <execution>
    20             <id>post-unit-test</id>
    21             <phase>test</phase>
    22             <goals>
    23                 <goal>report</goal>
    24             </goals>
    25             <configuration>
    26                 <dataFile>target/jacoco.exec</dataFile>
    27                 <outputDirectory>target/jacoco-ut</outputDirectory>
    28             </configuration>
    29         </execution>
    30     </executions>
    31 </plugin>
    View Code

    jacoco可以将单元测试的结果,直接生成html网页,分析代码覆盖率。注意 <outputDirectory>target/jacoco-ut</outputDirectory> 这一行的配置,表示将在target/jacoco-ut目录下生成测试报告。

    三、编写单测用例

    3.1 约定大于规范

    以OrderServiceImpl类为例,如果要对它做单元测试,建议按以下约定:

    a. 在test/java下创建一个与OrderServiceImpl同名的package名(注:这样的好处是测试类与原类,处于同1个包,代码可见性相同)

    b. 然后在该package下创建OrderServiceImplTest类(注意:一般测试类名的风格为 xxxxTest,在原类名后加Test)

    3.2 单元测试模板

    参考下面的代码模板:

    package com.cnblogs.yjmyzz.springbootdemo.service.impl;
    
    import com.cnblogs.yjmyzz.springbootdemo.service.UserService;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    import org.mockito.runners.MockitoJUnitRunner;
    
    @RunWith(MockitoJUnitRunner.class)
    public class OrderServiceImplTest {
    
        @Before
        public void setUp() {
            MockitoAnnotations.initMocks(this);
        }
        
        /**
         * 真正要测试的类
         */
        @InjectMocks
        private OrderServiceImpl orderService;
    
        /**
         * 测试类依赖的其它服务
         */
        @Mock
        private UserService userService;
    
        /**
         * createOrder成功时的用例
         */
        @Test
        public void testCreateOrderSuccess() {
            //todo
        }
    
        /**
         * createOrder失败时的用例
         */
        @Test
        public void testCreateOrderFailure() {
            //todo
        }
    
    }
    

    讲解一下:

    a. 类上的@RunWith要改成 MockitoJUnitRunner.class,否则mockito不生效

    b. 真正需要测试的类,要用@InjectMocks,而不是@Mock(更不能是@Autowired)

        -- 原因1:@Autowired是Spring的注解,在mock环境下,根本就没有Spring上下文,当然会注入失败。

        -- 原因2:也不能是@Mock,@Mock表示该注入的对象是“虚构”的假对象,里面的方法代码根本不会真正运行,统一返回空对象null,即:被@Mock修饰的对象,在该测试类中,其具体的代码永远无法覆盖到!这也就是失败了单元测试的意义。而@InjectMocks修饰的对象,被测试的方法,才会真正进入执行。

    另外,测试服务时,被mock注入的类,应该是具体的服务实现类,即:xxxServiceImpl,而不是服务接口,在mock环境中接口是无法实例化的。

    c. 通常一个方法,会有运行成功和运行失败二种情况,建议测试类里,用testXXXSuccess以及testXXXFailure区分开来,看起来比较清晰。

    3.3 测试覆盖率

    先来看看下单失败的情况:下单前有很多参数校验,先验证下这些参数异常的场景。

        public int userId = 101;
        
        /**
         * createOrder失败时的用例
         */
        @Test
        public void testCreateOrderWhenFail() {
            try {
                orderService.createOrder(null, 10, userId);
            } catch (Exception e) {
                Assert.assertEquals(true, true);
            }
    
            try {
                orderService.createOrder("book", null, userId);
            } catch (Exception e) {
                Assert.assertEquals(true, true);
            }
    
            try {
                orderService.createOrder("book", 0, userId);
            } catch (Exception e) {
                Assert.assertEquals(true, true);
            }
    
            try {
                orderService.createOrder("book", 50, userId);
            } catch (Exception e) {
                Assert.assertEquals(true, true);
            }
        }
    

    命令行下mvn package 跑一下单元测试,全通过后,会在target/jacoco-ut 目录下生成网页报告

    浏览器打开index.html,就能看到覆盖率

    可以看到,中间那个带部分绿色的,就是我们刚才写过单测的pacakge,一层层点下去,能看到OrderServiceImpl.createOrder方法的代码覆盖情况,绿色的行表示覆盖到了,红色的表示未覆盖。

    讲一个小技巧:有些类,比如DAO/Mytatis层自动生成的DO/Entity,还有一些常量定义等,其实没什么测试的必要,可以排除掉,这样不仅可以提高测试的覆盖率,还能让我们更关注于核心业务类的测试。

    排除的方法很简单,可jacoco插件里配置exclude规则即可,参考下面这样:

    <configuration>
        <dataFile>target/jacoco.exec</dataFile>
        <outputDirectory>target/jacoco-ut</outputDirectory>
        <excludes>
            <exclude>
                **/cnblogs/yjmyzz/**/aspect/**,
                **/yjmyzz/**/SampleApplication.class
            </exclude>
        </excludes>
    </configuration>
    View Code

    这样就把aspect包下的所有类,以及SampleApplication.class这个特定类给排除在单元测试之外,此时再跑一下mvn package ,对比下重新生成的报告

    覆盖率从刚才的26%上升到了61% 

    3.4 mock返回值

    从覆盖率上看,刚才createOrder方法里,最后几行并没有覆盖到,可以再写一个用例

    问题来了,报异常了!分析下UserService的queryBalance方法实现

        @Override
        public BigDecimal queryBalance(int userId) {
            System.out.println("queryBalance=>userId:" + userId);
            //模拟返回100元余额
            return new BigDecimal(100);
        }
    

    已经写死了返回100元,不应该为Null对象,同时还输出了一行日志,但是从测试结果来看,这个方法并没有真正执行。这也就印证了@Mock修饰的对象,是“假”的,并不会真正执行内部的代码

    @Test
    public void testCreateOrderSuccess() throws Exception {
        BigDecimal balance = BigDecimal.TEN;
        //表示:当userService.queryBalance(userId)执行时,将返回balance变量做为返回值
        when(userService.queryBalance(userId)).thenReturn(balance);
        long orderId = orderService.createOrder("phone", 10, userId);
        Assert.assertEquals(orderId, 1L);
    }
    

    把测试代码调整下,改成上面这样,利用when(...).thenReturn(...),表示当xxx方法执行时,将模拟返回yyy对象。这样就mock出了userService的返回值

    现在测试就通过了,再看看生成的测试报告,最后几行,也被覆盖到了。

  • 相关阅读:
    JavaScript之事件委托
    js中的事件委托(事件代理)详解
    CentOS已经安装命令,但提示找不到
    在Linux下创建7种类型的文件
    python源码安装的包的卸载
    新建文件所属组设置
    FFmpeg基础
    微服务架构概念
    一台 Java 服务器可以跑多少个线程?
    「学习的真正作用」​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​写出我心(一百三十八)
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/mockito-and-jacoco-tutorial.html
Copyright © 2011-2022 走看看