mock测试及jacoco覆盖率

摘要:
单元测试是确保项目代码质量的有力武器。然而,在某些业务场景中,依赖的第三方没有测试环境。此时我们如何进行UnitTest?这不能直接在生产环境中完成吗?“);}if{thrownewException;}//略微下订单,返回1L作为订单号LongorderId=1L;//模拟检测平衡BigDecimalbalance=userService.queryBalance;if{System.out.println(”余额不足10元,请及时充值!注意createOrder方法。OrderService的最后几行调用UserService来查询余额,也就是说,OrderService依赖于UserService。假设UserService是第三方服务,并且没有测试环境,本文将讨论如何模拟UserService。

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

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

一、准备示例Demo

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

mock测试及jacoco覆盖率第1张

 示例代码:

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

mock测试及jacoco覆盖率第2张mock测试及jacoco覆盖率第3张
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插件

mock测试及jacoco覆盖率第4张mock测试及jacoco覆盖率第5张
 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 目录下生成网页报告

mock测试及jacoco覆盖率第6张

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

mock测试及jacoco覆盖率第7张

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

mock测试及jacoco覆盖率第8张

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

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

mock测试及jacoco覆盖率第9张mock测试及jacoco覆盖率第10张
<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 ,对比下重新生成的报告

mock测试及jacoco覆盖率第11张

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

3.4 mock返回值

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

mock测试及jacoco覆盖率第12张

问题来了,报异常了!分析下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的返回值

mock测试及jacoco覆盖率第13张

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

mock测试及jacoco覆盖率第14张

免责声明:文章转载自《mock测试及jacoco覆盖率》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇centos8平台使用nethogs基于进程监控网络流量OVS 主机间虚拟机互通下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

[转]敏捷开发中编写高质量Java代码

本文转自:http://dev.yesky.com/103/11164603.shtml 敏捷开发的理念已经流行了很长的时间,在敏捷开发中的开发迭代阶段中,我们可以通过五个步骤,来有效的提高整个项目的代码质量。 Java项目开发过程中,由于开发人员的经验、Java代码编写习惯,以及缺乏统一的标准和管理流程,往往导致整个项目的代码质量较差,难于维 护,需要...

webpack4+vue2+axios+vue-router的多页+单页混合应用框架

VUE2的单页应用框架有人分享了,多页应用框架也有人分享了,这里就分享一个单页和多页的混合应用框架吧,初现雏形,还有很多需要优化和改善的地方。。。 结尾有github地址。 项目结构 │ ├─build /* webpack的配置目录 */ │ ├─config.js...

Android中的单元测试

随着Agile的普及,以及开发人员对测试重要性的认识逐步加深,单元测试已经成了越来越多软件项目开发中不可缺少的一部分。无论项目是不是采用TDD的形式来进行开发,单元测试都能够为项目的修改和重构提供一定的保障。 Android作为主要的移动平台之一,吸引了无数的开发人员。但面对Android平台和环境的种种限制,很多开发人员往往有心无力,很难为其项目添加...

Mockito各场景使用介绍

场景1:某三方接口所有方法都需要Mock 实现方式1-配置configrution bean 实现方式2-在application context中配置mock bean 场景2:某三方接口部分方法mock 实现方式1-spy方式: 实现方式2-callRealMethod(): 场景3:影响范围只在单个测试类 实现方式1-使用ReflectionTest...

mock的使用及取消,node模仿本地请求:为了解决前后端分离,用户后台没写完接口的情况下

借鉴:https://www.jianshu.com/p/dd23a6547114 1、说到这里还有一种是配置node模拟本地请求 (1)node模拟本地请求: 补充一下 【1】首先在根目录下建一个data.json,用来存放一些返回数据,名字随便取好了 [2]在webpack.dev.conf.js文件里 在这个const portfinder...

JaCoCo代码覆盖率从0到100的入门实践

JaCoCo全称是Java Code Coverage,Java代码覆盖率,广泛运用于各种测试平台对Java代码的全量覆盖率和增量覆盖率进行统计,分析代码行差异,度量单元测试效果。Jacoco也是精准测试的技术实现手段之一。 入门实践的目标是写点简单代码,再加点单元测试,把JaCoCo跑起来,输出测试报告,看代码覆盖率是怎么回事,了解基本的运行流程。 先写...