单元测试是保证项目代码质量的有力武器,但是有些业务场景,依赖的第三方没有测试环境,这时候该怎么做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>

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>

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>

这样就把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的返回值

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

mock测试及jacoco覆盖率的更多相关文章

  1. Jacoco覆盖率工具使用之maven篇

    说明 之前的文章已经介绍过如何使用apacheant 执行jacoco工具,下面开始介绍如何使用maven使用jacoco工具. 1.首先新建一个maven项目       如图所示:        ...

  2. Python单元测试和Mock测试

    单元测试 测试可以保证你的代码在一系列给定条件下正常工作 测试允许人们确保对代码的改动不会破坏现有的功能 测试迫使人们在不寻常条件的情况下思考代码,这可能会揭示出逻辑错误 良好的测试要求模块化,解耦代 ...

  3. mock测试到底是什么?

    ​    ​经常听人说mock测试,究竟什么是mock测试呢?mock测试能解决什么问题?mock测试要如何做呢?今天为大家做简单介绍,之后会有详细的mock测试,感谢大家对测试梦工厂的持续关注. 概 ...

  4. mock测试框架Mockito

    无论是敏捷开发.持续交付,还是测试驱动开发(TDD)都把单元测试作为实现的基石.随着这些先进的编程开发模式日益深入人心,单元测试如今显得越来越重要了.在敏捷开发.持续交付中要求单元测试一定要快(不能访 ...

  5. Spring MVC如何测试Controller(使用springmvc mock测试)

    在springmvc中一般的测试用例都是测试service层,今天我来演示下如何使用springmvc mock直接测试controller层代码. 1.什么是mock测试? mock测试就是在测试过 ...

  6. mock测试

    看到群里有人说mock测试,究竟什么是mock测试呢?开始自己也没明白,查了下相关资料.还是很有必要了解哈:那么mock测试能解决什么问题?mock测试要如何做呢?今天为大家做简单介绍.mock测试就 ...

  7. Postman入门之Mock测试

    1.什么是Mock测试: mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法. 2.添加要Mock测试的接口为example: 2.1点击r ...

  8. java的mock测试框架

    无论是敏捷开发.持续交付,还是测试驱动开发(TDD)都把单元测试作为实现的基石.随着这些先进的编程开发模式日益深入人心,单元测试如今显得越来越重要了.在敏捷开发.持续交付中要求单元测试一定要快(不能访 ...

  9. mock测试SpringMVC controller报错

    使用mock测试Controller时报错如下 java.lang.NoClassDefFoundError: javax/servlet/SessionCookieConfig at org.spr ...

  10. 【转】MOCK测试

    mock测试:就是在测试过程中,对于某些不容易构造或者 不容易获取的对象,用一个虚拟的对象[mock对象]来创建以便测试的测试方法. mock对象:这个虚拟的对象就是mock对象. mock对象就是真 ...

随机推荐

  1. 代码随想录第十八天 | Leecode 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236. 二叉树的最近公共祖先

    530. 二叉搜索树的最小绝对差 题目描述 给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 . 差值是一个正数,其数值等于两值之差的绝对值. 示例 1: 输入:roo ...

  2. c#开发完整的Socks5代理客户端与服务端(已完结)

    本文我们介绍下如何在Windows系统上开发一个代理本机流量的客户端,并且对接我们之前开发的Socks5服务端,实现整个代理的一条龙.对于Socks5代理的服务端的开发可以详见之前的文章. 目录 本机 ...

  3. Special Binary String——LeetCode进阶路

    原题链接https://leetcode.com/problems/special-binary-string/ 题目描述 Special binary strings are binary stri ...

  4. 9 easybr指纹浏览器https代理认证教程

    目的 在高匿名浏览环境中,代理是关键组件之一.相比普通 HTTP 代理,HTTPS 代理(HTTP over TLS) 支持加密传输,在保障隐私.防止中间人攻击方面更具优势. Chromium 浏览器 ...

  5. VSCode将本地项目代码上传到gitee中

    1.创建远程仓库,这个就是该仓库的地址   2.查看git的版本 git --version 3.使用git init命令初始化git 4.使用git status命令来查看文件是否被修改  : gi ...

  6. EDR(端点检测与响应)如何提升中小型企业(SMB)的网络安全

    1.什么是 EDR? (What is EDR?) Endpoint Detection and Response (EDR) is a cybersecurity solution... EDR t ...

  7. FastAPI认证系统:从零到令牌大师的奇幻之旅

    title: FastAPI认证系统:从零到令牌大师的奇幻之旅 date: 2025/06/06 16:13:06 updated: 2025/06/06 16:13:06 author: cmdra ...

  8. Springboot笔记<11>面向切面编程AOP

    面向切面编程AOP AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是Spring框架中 ...

  9. BAPI_CUSTOMERRETURN_CREATE 创建退货订单

    READ TABLE s_head INDEX 1. IF sy-subrc = 0. ls_orders_h = s_head. *** 抬头 CLEAR: ls_header,ls_headerx ...

  10. EDGE浏览器提示“无法安全下载……”

    EDGE浏览器升级后,下载文件时显示"无法安全下载 --"可以点击该下载后面的"-",再点击"保留",然后会弹出一个对话框,提示" ...