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目录下生成测试报告。

注:如果最终按本文方法,没有生成测试报告,可先检测下test/target目录下,是否生成了jacoco.exec文件。如果没有,尝试将jacoco插件升级到最新版本。另外JDK 17环境,还需要配置一些参数,参考下面:

<plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <configuration>
                    <dataFile>target/jacoco.exec</dataFile>
                    <outputDirectory>target/jacoco-ut</outputDirectory>
                    <excludes>
                        <exclude>
                            <!--需要排除test的部分,根据自己项目情况来-->
                            **/yjmyzz/dal/**,
                            **/yjmyzz/contract/**,
                            **/yjmyzz/**/constants/**,
                            **/yjmyzz/**/model/**,
                            **/yjmyzz/config/**,
                            **/yjmyzz/utils/**
                        </exclude>
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <id>prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>post-unit-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <reuseForks>true</reuseForks>
                    <argLine>
                        ${argLine}
                        -Xmx2048m
                        --add-opens java.base/jdk.internal.util.random=ALL-UNNAMED
                        --add-opens java.base/java.lang=ALL-UNNAMED
                        --add-opens java.base/java.lang.reflect=ALL-UNNAMED
                        --add-opens java.base/sun.reflect.annotation=ALL-UNNAMED
                        --add-opens java.base/java.math=ALL-UNNAMED
                        --add-opens java.base/java.util=ALL-UNNAMED
                        --add-opens java.base/sun.util.calendar=ALL-UNNAMED
                        --add-opens java.base/java.io=ALL-UNNAMED
                        --add-opens java.base/java.net=ALL-UNNAMED
                        --add-opens java.xml/com.sun.org.apache.xerces.internal.jaxp.datatype=ALL-UNNAMED
                    </argLine>
                </configuration>
            </plugin>

  

 

三、编写单测用例

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的返回值

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

posted @ 2020-09-09 22:19  菩提树下的杨过  阅读(4189)  评论(1编辑  收藏  举报