Mockito单测之道

Mockito单测之道

去年写过一篇《TestNG单元测试实战》文章,严格来讲算集成测试。

没看的小伙伴可直接看本篇即可,本质是单元测试框架不同,写法不一样。

单测定义

单元测试定义:

对软件中最小可测单元进行验证,可理解为对一个类中公有、私有方法的测试验证。

单元测试原则:

1. 快速的。
不依赖外部环境比如数据库mysql,不依赖springboot应用启动,本质是执行一个函数,一个方法,所以理应是能快速运行的。

2. 自动的。
单元测试应该是全自动执行的,非交互式的。

3. 独立的。
单元测试用例之间是独立运行的,用例互相之间无依赖,对外部资源也无依赖。

4. 可重复的。
单元测试是可重复执行的,在被测代码不变的情况下,每次执行结果是一致的。

单元测试用例主要有以下特点:

1. 不依赖外部环境和数据;
2. 不需要启动应用和初始化对象;
3. 需要使用@Mock来初始化依赖对象,用@InjectMocks来初始化测试对象;
4. 需要自己模拟依赖方法,指定什么参数返回什么值或异常;
5. 返回值确定,可用Assert相关方法进行断言;
6. 可以验证依赖方法的调用次数和参数值,还可以验证依赖对象的方法是否调用完毕。

什么是有效的单元测试

  1. 验证依赖方法,验证调用次数;

  2. 使用明确语义的断言;

  3. 验证异常抛出;

  4. 验证数据对象属性;

  5. 用mock对象代替真实对象的注入;
    ......

单元测试和集成测试区别:

单元测试是对软件设计中最小单元的程序模块进行测试,集成测试是对这些程序模块组装成的系统模块进行测试。(单元测试没有外部依赖,集成测试也可能没有外部依赖)

集成测试用例主要有以下特点:

1. 依赖外部环境和数据;
2. 需要启动应用并初始化测试对象;
3. 使用自动注入机制注入测试对象;
4. 返回值具有不确定性,无法验证。

综上所述,为了更好的验证软件质量,更容易的去测试业务逻辑,推荐编写单元测试来完成代码的用例编写。

实战

环境依赖引入

springboot版本:2.2.2.RELEASE

引入Maven:

<!-- springboot项目加入junit依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-starter-test 依赖中包含了junit 和 mockito框架。

本文用Junit5 + Mockito 来编写单测。

Junit5 = Junit Platform + Junit Jupiter + Junit Vintage

IDEA工具推荐下载 TestMe 插件,自动生成用例类,生成通用模板后再进行修改。

项目案例编写

从一个简单的例子入手

被测 service 对象:

@Service
public class UserServiceImpl {

    @Autowired
    private IOrderService orderService;

    private static List<Long> ids;

    @Override
    public int activeNumber() {
        LambdaQueryWrapper wrapper = Wrappers.<Order>lambdaQuery()
                .in(Order::getId, ids);
        int count = orderService.count(wrapper);
        return count;
    }
}

单元测试用例类:

public class UserServiceImplTest {

    /**
     * 定义测试对象
     */
    @InjectMocks
    UserServiceImpl userServiceImpl;

    /**
     * 模拟依赖对象
     */
    @Mock
    IOrderService orderService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    void testActiveNumber() {
        Mockito.doReturn(1).when(orderService).count(Mockito.any());
        int result = userServiceImpl.activeNumber();
        Assertions.assertEquals(1, result, "数据结果不一致");
        Mockito.verify(orderService).count(Mockito.any());
    }
}

使Mockito的注解生效:

Junit4中:

  1. 类上添加 @RunWith(MockitoJUnitRunner.class)。

  2. @Before注解方法加 MockitoAnnotations.openMocks(this)。

Junit5中:

  1. 类上添加 @ExtendWith(MockitoExtension.class)。

  2. @BeforeEach注解方法加 MockitoAnnotations.initMocks(this)。(版本不同,写法不一样)

一个有依赖的单元测试实施步骤:

一. 定义被测对象。

用@Mock、@Spy、@InjectMocks 相关注解定义测试对象。

  • @Mock:该注解创建一个Mock对象,调用其方法时不会走真实方法

  • @Spy:该注解创建了一个没有Mock的对象,调用其方法是会走真实方法

  • @InjectMocks:创建一个实例,调用代码会走真实方法。会自动注入@Spy和@Mock标注的对象。

/**
 * 定义测试对象
 */
@InjectMocks
UserServiceImpl userServiceImpl;

/**
 * 模拟依赖对象
 */
@Mock
IOrderService orderService;

如果有静态类成员字段需要注入: 可用ReflectonTestUtils工具注入属性。

ReflectionTestUtils.setField(userServiceImpl, "ids", ids);

二. 模拟依赖对象

模拟依赖对象方法返回,包含参数的返回、异常的返回等。

//模拟orderService.count() 方法调用,调用时返回值为1。
Mockito.doReturn(1).when(orderService).count(Mockito.any());

Mockito.doReturn().when() 和 Mockito.when().thenReturn() 都用于模拟对象方法,在Mock实例下都可使用。

但在 Spy实例下使用时, when().thenReturn() 模式会执行原方法,而 doReturn().when()模式不会执行原方法。

Mock实例下

Mockito.doReturn().when() Mockito.when().thenReturn() 都会走模拟方法。 

Spy实例下

Mockito.doReturn().when() 走模拟调用
Mockito.when().thenReturn() 走真实调用

推荐使用doReturn/when。如果不关心具体的参数内容,可用Mockito.any() 代替。

三. 调用被测对象

//模拟依赖方法
int result = userServiceImpl.activeNumber();
//调用被测对象
Assertions.assertEquals(1, result, "数据结果不一致");
Mockito.verify(orderService).count(Mockito.any());

代码中的例子比较简单,假如要模拟的参数是一个对象,则可用JSONObject来验证,提前创建json字符串在文件中,最终转成字符串比较即可。

//例子  

String text = ResourceHelper.getResourceAsString(getClass(), "text.json");
List<T> list = JSON.parseArray(text, T.class);
Mockito.doReturn(list).when(service).list(Mockito.any());
  
//调用依赖方法
List<Map<String, String>> result = serviceA.method();

String text_expected = ResourceHelper.getResourceAsString(getClass(), "text_expected.json");

//映射排序字段,保证key、value字段有序性
Assertions.assertEquals(text_expected, JSON.toJSONString(result, SerializerFeature.MapSortField));
Mockito.verify(service).list(Mockito.any());
//getResourceAsString() 来自参考资料。
  
public static <T> String getResourceAsString(Class<T> clazz, String name) {
    try (InputStream is = clazz.getClassLoader().getResourceAsStream(name)) {
        return IOUtils.toString(is, StandardCharsets.UTF_8);
    } catch (IOException e) {
        throw new IllegalArgumentException(String.format("以字符串方式获取资源(%s)异常", name), e);
    }
}

四. 验证方法调用

确认被测方法是否按预期方法进行了调用。

<span style="color:red"><span style="color:red"><span style="color:red"><code>//验证依赖方法,默认验证调用一次
Mockito.verify(orderService).count(Mockito.any());
</code></span></span></span>

验证多次调用可通过 Mockito.times()调整,Mockito还支持 最少一次调用最多一次调用等方法验证。

验证调用3次

Mockito.verify(service, Mockito.times(3)).count(Mockito.any());

也可通过 verifyNoMoreInteractions() 验证依赖对象,以确保所有调用都得到验证。

单测技巧

1. 使用JSON序列化简化预期值的比较。

在test/resources 目录下,定义预期值的 json文件。减少验证对象代码的编写。

//和 getResourceAsString() 方法类似,读取json文件数据。
  
  
try {
    InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(<span style="color:#50a14f">"testActiveNumberWithResult.txt"</span>);
    String text = IOUtils.toString(is, <span style="color:#50a14f">"UTF-8"</span>);
    //JSON转成对应的list、map即可
    List list = JSON.parseObject(text, List.class);
} catch (IOException e) {
    //捕获异常
    throw new RuntimeException(e);
}

2、通过Assert.assertThrows 来验证方法异常抛出。

支持验证自定义属性、支持验证依赖方法及其参数。

Assertions.assertThrows(RuntimeException.class, () -> Integer.parseInt("1"), "未返回期望异常");

3. ArgumentCaptor 类捕获参数值。

Mockito 提供 ArgumentCaptor 类来捕获参数值,通过调用 forClass(Class<T> clazz) 方法来构建一个 ArgumentCaptor 对象,然后在验证方法调用时来捕获参数,最后获取到捕获的参数值并验证。

如果一个方法有多个参数都要捕获并验证,那就需要创建多个 ArgumentCaptor 对象。

ArgumentCaptor 的主要接口方法:

  • capture():用于捕获方法参数;

  • getValue():用于获取捕获的参数值,如果捕获了多个参数值,该方法只返回最后一个参数值;

  • getAllValues():用户获取捕获的所有参数值。

//代码实例

ArgumentCaptor<Wrapper> captor = ArgumentCaptor.forClass(Wrapper.class);
Mockito.verify(service).list(captor.capture());
Wrapper wrapper = captor.getValue();

参考资料

  1. https://developer.aliyun.com/ebook/7895

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/6185.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】链表练习题(2)

链表练习题1.相交链表(LeetCode160)2.环形链表(LeetCode141)3.环形链表Ⅱ(LeetCode142)1.相交链表(LeetCode160) 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。题目数据 保…

spring注解的使用

Spring的一个核心功能是IOC&#xff0c;就是将Bean初始化加载到容器中&#xff0c;Bean是如何加载到容器的&#xff0c;可以使用Spring注解方式或者Spring XML配置方式。 Spring注解方式减少了配置文件内容&#xff0c;更加便于管理&#xff0c;并且使用注解可以大大提高了开发…

你看这个spring的aop它又大又宽

aop&#x1f693;AOP 分类AspectJ | 高级但是难用Spring AOP | 易用但仅支持方法aop 原理明月几时有&#xff0c;把酒问青天。——唐代李白《将进酒》 AOP 分类 在 Spring Boot 中&#xff0c;AOP 的实现主要有以下几种&#xff1a; 基于 AspectJ 的 AOP&#xff1a;这是一种基…

数据结构——红黑树

目录 概念 性质 结点的定义 插入 调整 当p是g的左孩子时 当p为g的右孩子时 插入完整代码 红黑树的检测 红黑树完整代码&#xff08;包括测试数据&#xff09; 概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&…

如何有效备考PMP?

随着PMP证书含金量直线上升&#xff01;现在PMP证书就跟黄金一样&#xff0c;即保值又升值。 今天小编应势出一篇关于如何高效备考PMP的方法&#xff0c;在备考生快过来看看吧&#xff01; 1、准备好所需要的教材&#xff0c;视频&#xff0c;试题内容 备考备考&#xff0c;你…

蓝桥杯刷题冲刺 | 倒计时5天

作者&#xff1a;指针不指南吗 专栏&#xff1a;蓝桥杯倒计时冲刺 &#x1f43e;马上就要蓝桥杯了&#xff0c;最后的这几天尤为重要&#xff0c;不可懈怠哦&#x1f43e; 文章目录1.方格迷宫2.字符串删减1.方格迷宫 题目 链接&#xff1a; 4943. 方格迷宫 - AcWing题库 给定一…

Sam Altman专访:GPT-4没太让我惊讶,ChatGPT则让我喜出望外

导读ChatGPT、GPT-4 无疑是 2023 年年初人工智能界最大的「爆款」。3 月 26 日&#xff0c;OpenAI CEO、ChatGPT 之父 Sam Altman 接受了著名学者与科技播客、麻省理工大学研究员 Lex Fridman 的专访&#xff0c;Sam 分享了从OpenAI内部视角如何看待ChatGPT和GPT-4的里程碑式意…

分享:数据库存储与索引技术(三)LSM树实现案例

欢迎访问 OceanBase 官网获取更多信息&#xff1a;https://www.oceanbase.com/ 本文来自OceanBase社区分享&#xff0c;仅限交流探讨。原作者马伟&#xff0c;长期从事互联网广告检索系统的研发&#xff0c;对数据库&#xff0c;编译器等领域也有浓厚兴趣。 文章目录1. MemTab…

2.2.2 第2遍:程序细节

这段话主要解释了C程序中#include指令和头文件的作用。头文件包含了编译器所需的信息&#xff0c;例如函数名、常量、以及如何使用它们等。在C程序中&#xff0c;头文件通常用于包含库函数&#xff0c;例如stdio.h文件中包含了输入和输出函数&#xff08;如printf()&#xff09…

LCHub:ChatGPT4和低代码来临,程序员面临下岗?

一个网友吐槽道: “ 建站出来了,你们说程序员会失业。 低代码出来了,你们说程序员会失业。 Copilot出来了,你们说程序员会失业。 Chatgpt出来了,你们说程序员会失业 虽然这只是网友的吐槽,但却引起了小编的好奇。为何程序员那么容易被新技术取代?今天小编打算跟大家…

Waline在Butterfly主题中的应用

LeanCloud 设置 (数据库) 国内版的LeanCloud需要绑定域名&#xff0c;所以我们直接选择国外版的LeanCloud 登陆注册 注册&#xff1a;点击这里进行跳转注册成功后进入控制台&#xff0c;选择 创建应用 。 创建完成后进入应用&#xff0c;下拉找到 设置 , 会有 AppID 、AppK…

ASO优化之应用商店关键词的实现

投放正确的合适的关键词&#xff0c;能够确保我们的应用获得更高的相关性和知名度。如果我们已经完成研究并想要竞争目标关键词&#xff0c;就需要在商品详情中去实施投放它们。 要在 Google Play Store 中投放——我们要打开 Google Play 控制台并点击“主要应用详情”选项卡…

基于模型预测控制(MPC)的微电网调度优化的研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

VMware创建和使用虚拟网络

文章目录如何打开虚拟网络编辑器让虚拟机使用有线、无线网卡1. 点击“添加网络”2. 虚拟机使用电脑自带无线网卡3. 虚拟机使用电脑自带有线网卡重置虚拟网络在使用虚拟机的过程中&#xff0c;有时会需要让虚拟机使用物理机的网络设备直接与外部连接&#xff0c;例如让虚拟机通过…

Win11启用IE方法

呉師傅 Win11是微软目前的最新系统&#xff0c;尽管该系统非常不错&#xff0c;但是还是有很多不一样的地方&#xff0c;有的用户发现Win11没有了IE浏览器&#xff0c;那么Win11没有IE浏览器怎么办呢&#xff0c;有的旧网页需要IE浏览器才能进入&#xff0c;下面就给大家提供一…

怎么把两个音频合成一个

在创作音乐、制作视频等领域&#xff0c;经常需要将音频文件进行合并处理&#xff0c;但对于没有专业工具和知识的朋友来说&#xff0c;音频合并可能是一项复杂的任务。本篇文章就要为大家介绍合并音频的方法&#xff0c;让大家能够快速地将音频文件合并成需要的部分&#xff0…

leaflet: 地图上叠加日夜区域(126)

第126个 点击查看专栏目录 本示例的目的是介绍如何在vue+leaflet中显示日夜交替叠加区域。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共68行)安装插件相关API参考:专栏目标示例效果 配置方式 1)查看基础设…

ChatGPT能胜任高级程序员吗?

与开发人员信任的其他软件开发工具不同&#xff0c;AI工具在训练、构建、托管和使用方式等方面都存在一些独特的风险。 自2022年底ChatGPT发布以来&#xff0c;互联网上便充斥着对其几乎相同比例的支持和怀疑的论调。不管你是否喜欢它&#xff0c;AI正在逐步进入你的开发组织。…

【设计模式】Bridge Design pattern 桥接模式

1.桥接模式要解决的问题 多个维度的变化引起的继承组合指数级增长 例子 一个物体有不同形状和不同颜色&#xff0c;如何用类来表示它们&#xff0c;这里包含了两个变化维度&#xff0c;一个是物体的形状&#xff0c;一个是颜色 继承的方式 如果使用继承的方式&#xff0c;此…

抖音seo优化系统常见的交付形式|技术开发

1. 一次性交付&#xff1a;将整个SEO优化系统一次性交付给客户&#xff0c;包括相关的文档、工具和数据分析报告&#xff0c;由客户自行操作和维护。 2. 阶段性交付&#xff1a;将SEO优化系统分为不同的阶段进行交付&#xff0c;每个阶段完成后进行检查和评估&#xff0c;根据…