深入解析与实践Mockito:Java单元测试的强大助手

2024-04-15 20:04

本文主要是介绍深入解析与实践Mockito:Java单元测试的强大助手,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、Mockito概念

引言

Mockito是Java生态系统中最受欢迎的单元测试模拟框架之一,以其简洁易用的API和强大的模拟能力赢得了广大开发者的青睐。Mockito允许我们在不实际依赖外部资源的情况下对代码进行彻底且高效的单元测试,极大地提升了测试覆盖率和代码质量。

什么是Mockito

Mockito是一种模拟框架,其核心概念是在测试过程中创建并使用“Mock对象”。Mock对象是对实际对象的一种模拟,它继承或实现了被测试类所依赖的接口或类,但其行为可以根据测试需求自由定制。控制其在测试环境下的行为,从而将注意力聚焦于类本身的逻辑验证上。

Mockito的优势

  • 隔离度高:通过模拟依赖,减少测试间的耦合,确保单元测试真正只关注被测试单元的内部逻辑。
  • 易于使用:API设计直观简洁,降低了编写和阅读测试用例的难度。
  • 详尽的验证:能够准确跟踪和验证被测试对象与其依赖之间的交互行为。
  • 灵活性强:支持多种定制模拟行为,无论是简单的返回值还是复杂的回调机制。
  • 有利于TDD实践:与测试驱动开发方法论紧密契合,鼓励写出更易于测试的代码。

二、Mockito的主要功能点和方法使用

Mock 方法

  1. Mock对象创建

使用Mockito.mock()方法创建接口或抽象类的Mock对象。

public static <T> T mock(Class<T> classToMock)
  • classToMock:待 mock 对象的 class 类。
  • 返回 mock 出来的类

实例:使用 mock 方法 mock 一个类

Random random = Mockito.mock(Random.class);

Mock对象进行行为验证和结果断言

使用 verify 验证

验证是校验对象是否发生过某些行为,Mockito 中验证的方法是:verify

verify(mock).someMethod("some arg");
verify(mock, times(1)).someMethod("some arg");

验证交换:Verify 配合 time() 方法,可以校验某些操作发生的次数。
注意:当使用 mock 对象时,如果不对其行为进行定义,则 mock 对象方法的返回值为返回类型的默认值。

    /*** 测试Mockito框架的使用,模拟Random类的nextInt方法。* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test01() {// 使用Mockito模拟一个Random对象Random random = Mockito.mock(Random.class);// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)System.out.println("第一次:"+random.nextInt());// 指定当调用nextInt()时,始终返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1// 断言nextInt()方法返回值是否为1Assertions.assertEquals(1,random.nextInt());// 验证nextInt()方法是否被调用了两次verify(random, times(3)).nextInt();}

断言使用到的类是 Assertions

Random random = Mockito.mock(Random.class);
Assertions.assertEquals(2, random.nextInt());

输出结果:断言nextInt()方法返回值是否为1

org.opentest4j.AssertionFailedError: 
Expected :2
Actual   :1

Mock对象打桩

打桩可以理解为mock对象规定一行的行为,使其按照我们的要求来执行具体的操作。在Mockito中,常用的打桩方法为

方法含义
when().thenReturn()Mock 对象在触发指定行为后返回指定值
when().thenThrow()Mock 对象在触发指定行为后抛出指定异常
when().doCallRealMethod()Mock 对象在触发指定行为后调用真实的方法

thenReturn() 代码示例

@Test
void check() {Random random = Mockito.mock(Random.class);Mockito.when(random.nextInt()).thenReturn(100);Assertions.assertEquals(100, random.nextInt());
}

Mock 静态方法

首先要引入 Mockito-Inline 的依赖

<dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>4.3.1</version><scope>test</scope>
</dependency>

使用 mockStatic() 方法来 mock静态方法的所属类,此方法返回一个具有作用域的模拟对象。

    /*** 测试方法 test01,用于验证 StringUtils 类的 joinWith 方法的功能。* 该方法模拟了静态方法 StringUtils.joinWith 的行为,以检查其是否能正确地将列表元素用指定分隔符连接成一个字符串。*/@Testpublic void testJoinWith() {// 使用 Mockito 框架模拟 StringUtils 类的静态方法MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);// 创建一个字符串列表,作为 joinWith 方法的输入参数List<String> stringList = Arrays.asList("a", "b", "c");// 配置模拟行为,当调用 StringUtils.joinWith(",", stringList) 时,返回 "a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");// 断言验证模拟行为是否正确,即 joinWith 方法返回的字符串是否与预期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));}
/*** 测试StringUtils类中的join方法。* 该测试使用Mockito框架来模拟静态方法的行为,验证join方法是否按照预期工作。* */@Testpublic void testJoin() {// 使用Mockito模拟StringUtils类的静态方法MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);// 创建一个字符串列表作为join方法的输入List<String> stringList = Arrays.asList("a", "b", "c");// 配置模拟行为,当调用StringUtils.join(",", stringList)时,返回字符串"a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.join(",", stringList)).thenReturn("a,b,c");// 断言验证模拟行为是否正确,即 join 方法返回的字符串是否与预期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.join(",", stringList).equals("a,b,c"));}

执行整个测试类后会报错:

org.mockito.exceptions.base.MockitoException: 
For com.echo.mockito.Util.StaticUtils, static mocking is already registered in the current threadTo create a new mock, the existing static mock registration must be deregistered

原因是因为 mockStatic() 方法是将当前需要 mock 的类注册到本地线程上(ThreadLocal),而这个注册在一次 mock 使用完之后是不会消失的,需要我们手动的去销毁。如过没有销毁,再次 mock 这个类的时候 Mockito 将会提示我们 :”当前对象 mock 的对象已经在线程中注册了,请先撤销注册后再试“。这样做的目的也是为了保证模拟出来的对象之间是相互隔离的,保证同时和连续的测试不会收到上下文的影响。
因此我们修改代码:

public class MockitoStaticTest {/*** 测试方法 test01,用于验证 StringUtils 类的 joinWith 方法的功能。* 该方法模拟了静态方法 StringUtils.joinWith 的行为,以检查其是否能正确地将列表元素用指定分隔符连接成一个字符串。*/@Testpublic void testJoinWith() {// 使用 Mockito 框架模拟 StringUtils 类的静态方法try (MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class)) {// 创建一个字符串列表,作为 joinWith 方法的输入参数List<String> stringList = Arrays.asList("a", "b", "c");// 配置模拟行为,当调用 StringUtils.joinWith(",", stringList) 时,返回 "a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");// 断言验证模拟行为是否正确,即 joinWith 方法返回的字符串是否与预期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));}}/*** 测试StringUtils类中的join方法。* 该测试使用Mockito框架来模拟静态方法的行为,验证join方法是否按照预期工作。*/@Testpublic void testJoin() {// 使用Mockito模拟StringUtils类的静态方法try (MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class)) {// 创建一个字符串列表作为join方法的输入List<String> stringList = Arrays.asList("a", "b", "c");// 配置模拟行为,当调用StringUtils.join(",", stringList)时,返回字符串"a,b,c"stringUtilsMockedStatic.when(() -> StringUtils.join(",", stringList)).thenReturn("a,b,c");// 断言验证模拟行为是否正确,即 join 方法返回的字符串是否与预期的 "a,b,c" 相等Assertions.assertTrue(StringUtils.join(",", stringList).equals("a,b,c"));}}}

thenThrow 方法定义

   /*** 测试当调用add方法时抛出RuntimeException异常的情况。* 该测试函数不接受参数,也没有返回值。*/@Testvoid testAddException() {// 设置mock对象,在调用mockitoTestController的add方法时抛出RuntimeException异常when(mockitoTestController.add(1, 2)).thenThrow(new RuntimeException("add error"));// 验证是否抛出了RuntimeException异常Assertions.assertThrows(RuntimeException.class, () -> mockitoTestController.add(1, 2));}

三、Mockito 中常用注解

可以代替 Mock 方法的 @Mock 注解

Shorthand for mocks creation - @Mock annotation
Important! This needs to be somewhere in the base class or a test runner:

快速 mock 的方法,使用 @mock 注解。
mock 注解需要搭配 MockitoAnnotations.openMocks(testClass) 方法一起使用。

@Mock
private Random random;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}/*** 测试Mockito框架的使用,模拟Random类的nextInt方法。* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test02() {// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)System.out.println("第一次:"+random.nextInt());// 指定当调用nextInt()时,始终返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1// 断言nextInt()方法返回值是否为1Assertions.assertEquals(1,random.nextInt());// 验证nextInt()方法是否被调用了两次verify(random, times(3)).nextInt();}

@BeforeEach 与 @BeforeAfter 注解

@Slf4j(topic = "RandomTest02")
public class RandomTest02 {@Mockprivate Random random;@BeforeEachvoid setUp() {log.info("==============测试前准备===============");MockitoAnnotations.openMocks(this);}/*** 测试Mockito框架的使用,模拟Random类的nextInt方法。* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test02() {// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)System.out.println("第一次:"+random.nextInt());// 指定当调用nextInt()时,始终返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1// 断言nextInt()方法返回值是否为1Assertions.assertEquals(1,random.nextInt());// 验证nextInt()方法是否被调用了两次verify(random, times(3)).nextInt();}@AfterEachvoid tearDown() {log.info("==============测试后结果===============");}}

而在 Junit5 中,@Before 和 @After 注解被 @BeforeEach 和 @AfterEach 所替代。

Spy 方法与 @Spy 注解

spy() 方法与 mock() 方法不同的是

  1. 被 spy 的对象会走真实的方法,而 mock 对象不会
  2. spy() 方法的参数是对象实例,mock 的参数是 class

示例:spy 方法与 Mock 方法的对比

    /*** 测试方法,检查 Mockito 框架的使用。* 无参数。* 无返回值,但期望通过断言验证操作结果。*/@Testvoid check() {// 调用实际的 mockitoTestController 对象的 add 方法,并验证结果是否为预期值int result = mockitoTestController.add(1, 2);Assertions.assertEquals(3, result);// 使用 Mockito 创建 mockitoTest 的 mock 对象,并对它调用 add 方法,然后验证结果MockitoTestController mockitoTest = Mockito.mock(MockitoTestController.class);int result1 = mockitoTest.add(1, 2);Assertions.assertEquals(3, result1);}

输出结果

// 第二个 Assertions 断言失败,因为没有给 mockitoTest 对象打桩,因此返回默认值
org.opentest4j.AssertionFailedError: 
Expected :3
Actual   :0

使用 @Spy 注解代码示例

@Slf4j
@ExtendWith(MockitoExtension.class)
class MockitoTestControllerTest {@Spyprivate MockitoTestController mockitoTestController;@BeforeEachvoid setUp() {}/*** 测试add方法* 该方法模拟调用mockitoTestController的add方法,传入参数1和2,期望返回值为3。* 首先,通过when语句设置mockitoTestController的add方法返回值为3;* 然后,使用assertThat断言验证调用add方法(1, 2)实际返回值确实为3;* 最后,通过verify语句确认mockitoTestController的add方法确实被调用了一次,并传入了参数1和2。*/@Testvoid testAdd() {// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4when(mockitoTestController.add(1, 2)).thenReturn(4);// 调用mock对象的方法,返回为4int result = mockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 断言验证:调用add(1, 2)方法返回值是否为4assertThat(mockitoTestController.add(1, 2)).isEqualTo(4);// 验证:确保add方法(1, 2)被调用了一次verify(mockitoTestController,times(2)).add(1, 2);}
}

四、Mockito使用总结

总结来说,Mockito通过模拟依赖、设置行为预期、验证交互和处理异常等方式,极大地增强了Java单元测试的可靠性和效率。无论是在小型项目还是大型企业级应用中,Mockito都是提升测试覆盖率和代码质量不可或缺的工具。

五、参考文档

  1. Mockito 中文文档 ( 2.0.26 beta ) - 《Mockito 框架中文文档》 - 极客文档
  2. Spring Boot集成单元测试之如何mock_springboot mock测试-CSDN博客

下一篇链接:Spring Boot 整合 Mockito:提升Java单元测试的高效实践。

Git项目地址-对应的project:springboot-mockito-study

欢迎大家一键三连,如果文章中有错误或遗漏的地方,欢迎大家指正!

这篇关于深入解析与实践Mockito:Java单元测试的强大助手的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/906786

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2