当前位置: 首页 > news >正文

Java 单元测试框架之 Mockito 详细介绍


本文是博主在学习如何高效创建单元测试时的知识记录,文中项目代码是基于 SpringBoot 项目,测试组件使用的 JUnit 5,单元测试组件使用的 Mockito 。虽然现在都是在使用 AI 助手帮助生成单元测试和代码辅助修改,但我们不能被工具挡住了要了解知识的目的,学会知识才是根本所在。
网图


文章目录

  • 一、Mockito 是什么?
  • 二、Mockito 的使用方法
    • 1、使用 Maven 引入依赖
    • 2、使用 Gradle 引入依赖
    • 3、如何使用 mock 和 when 方法
  • 三、Mockito 常见注解
    • 1、@ExtendWith(MockitoExtension.class)
    • 2、@Mock
    • 3、@Spy
    • 4、@InjectMocks
    • 5、@Captor
    • 6、@BeforeEach / @BeforeAll
    • 7、示例一
    • 8、示例二
    • 9、@Captor 示例
  • 四、注意事项

一、Mockito 是什么?

Mockito 是一个基于行为驱动开发(BDD)的模拟框架,用于Java的流行的单元测试框架,主要用于创建和管理模拟对象,使得编写单元测试变得更加简单和高效。

它允许开发者创建模拟对象,这些模拟对象可以模仿真实对象的行为,帮助开发者在隔离的环境中测试代码。通过使用 Mockito ,开发者可以专注于测试单个类的功能,而无需依赖外部系统或其他复杂的对象。

简单的说,就是某些方法调用的服务没有启动的话,Mockito 可以帮你模拟这个服务的返回值,让你可以只专注于自己的业务代码的开发。

用官方的换描述 Mockito 就是以下的几点:

  • 隔离测试:在单元测试中,需要隔离被测试的类与其他外部依赖,这样可以确保测试只关注被测试类的逻辑,而不受其他组件的影响。
  • 简化测试编写:Mockito 提供了简洁的 API ,使得创建和配置模拟对象变得容易,从而减少了测试代码的编写量,提供了大量的注解辅助完成测试用例的编写。
  • 提高测试的可靠性:通过模拟外部依赖,可以避免因外部系统的不稳定或不可用导致测试失败。

二、Mockito 的使用方法

1、使用 Maven 引入依赖

1、Spring Boot 2.2 及以上版本默认使用 JUnit 5 ,如果使用的是 JUnit 5 进行测试,需要引入 spring-boot-starter-test 依赖,它已经包含了 MockitoJUnit 5 的相关依赖。

pom.xml 中添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-starter-test 是 Spring Boot 提供的一个测试启动器,它集成了多种测试框架和工具,其中就包括Mockito。由于 scope 设置为 test ,这些依赖只会在测试环境中生效。

2、如果你不想使用 spring-boot-starter-test ,也可以单独引入 Mockito 核心依赖。

pom.xml 中添加以下依赖:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.11.0</version> <!-- 可根据需要选择合适的版本 -->
    <scope>test</scope>
</dependency>

同时,如果你使用 JUnit 5 进行测试,还需要单独引入 JUnit 5 的依赖:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>

3、扩展:Mockito 还提供了一些扩展,如Mockito Inline,可以用于模拟静态方法和私有方法等。如果你想要深入研究Mockito ,那一定要试试这些扩展。

pom.xml 中添加以下依赖:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.11.0</version> <!-- 可根据需要选择合适的版本 -->
    <scope>test</scope>
</dependency>

2、使用 Gradle 引入依赖

1、如果你使用 Gradle 构建项目,并且结合 JUnit 5 进行测试。

build.gradle 中添加以下依赖。

testImplementation 'org.springframework.boot:spring-boot-starter-test'

这行代码会引入 spring-boot-starter-test 依赖,其中包含了 Mockito 和 JUnit 5 的相关依赖。

2、如果你不想使用spring-boot-starter-test,可以单独引入Mockito核心依赖和JUnit 5的依赖。

build.gradle 中添加以下代码:

testImplementation 'org.mockito:mockito-core:4.11.0' // 可根据需要选择合适的版本
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-engine'

3、扩展:如果需要使用 Mockito Inline 扩展,可以在 build.gradle 中添加以下依赖:

testImplementation 'org.mockito:mockito-inline:4.11.0' // 可根据需要选择合适的版本

在 Spring Boo t项目中引入 Mockito 可以通过引入 spring-boot-starter-test 依赖的方式,也可以单独引入 Mockito 核心依赖和相关测试框架依赖。同时,还可以根据需要引入 Mockito 的扩展依赖。

3、如何使用 mock 和 when 方法

如果在测试用例中仅​使用 Mockito 进行测试时候,使用的是 Mockito 环境, 这个时候不会加载 SpringBoot 上下文,@Autowired 等注解不会起作用,这个时候手动使用 @Mock 和 @InjectMock 来处理类之间的依赖关系。

  • 在下面的示例不会加载 SpringBoot 上下文,会定位到 SpringBoot 的方法中。
  • 使用 Mockito.mock() 方法可以创建模拟对象。例如数据库的 Mapper 对象,用于调用数据库获取数据。
  • 使用 Mockito.when() 方法可以定义模拟对象的行为,这步很重要,模拟对象没办法拥有服务对象的功能。
  • 例如,在业务逻辑中存在调用数据库数据返回对象这样一个方法: 模拟对象.select(日期)
  • 我们使用 when(模拟对象.select(日期)).thenReturn(new User("John", 18, "男")); 这个语句定义模拟行为。
  • 即业务逻辑中 Mapper..select(日期) 会得到 new User("John", 18, "男") ,模拟对象也要拥有这个能力。
  • when 的意思就是如果遇到这个方法就返回 new User("John", 18, "男") 的结果,thenReturn 就是返回的意思。
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Test
    public void testSomeMethod() {
    	// 首先使用 mock 方法创建一个模拟对象
        UserDao userDao = mock(UserDao.class);
		// 给这个模拟对象做一个设定,当 userDao.findById(1) 方法时,
        when(userDao.findById(1)).thenReturn(new User("John"));

        UserService userService = new UserService(userDao);
        // 注意:userService.findUserById(1) 即为 在这个方法中会传递到 userDao.findById(1)
        User user = userService.findUserById(1);
        assertEquals("John", user.getUsername());
        
        // 验证是否调用了 userDao.findById(1)
        verify(userDao).findById(1);

		// 模拟方法调用:除了返回固定值,还可以模拟抛出异常、返回不同值等复杂行为。
		when(userDao.findById(1)).thenThrow(new RuntimeException());
	
		// 参数匹配:可以使用参数匹配器来匹配方法调用的参数。
		when(userDao.findById(anyInt())).thenReturn(new User("John"));
		when(userDao.findByName(anyString())).thenReturn(new User("AnyName"));
    }
}

详细说一下 when 的用法,这个通常用在参数输入上,例如在上面使用了 userDao.findById(1) 方法,这里传了参数 1 ,但是很多时候我们项目中类似的业务代码都会传一些复杂对象,我们去构建这个复杂对象无疑是麻烦的。

这时,我们可以这样用 userDao.findById(anyInt()) ,对于基本类型 any() 方法都有支持,如果是日期则使用 any(LocalDate.class) ,如果是其他的 Object 类型则使用 any(Object.class)

三、Mockito 常见注解

Mockito 提供了多个注解,这些注解有助于简化测试代码,使测试结构更加清晰和易于维护。

1、@ExtendWith(MockitoExtension.class)

@ExtendWith(MockitoExtension.class) 用于将 Mockito 集成到 JUnit 5 测试框架。使用该注解后,JUnit 5 会自动初始化标记为 @Mock、@InjectMocks 等注解的对象,而无需手动调用 MockitoAnnotations.initMocks(this)。它使得在单元测试中使用 Mockito 更为便捷,可以快速创建模拟对象(mocks)、间谍对象(spies),并注入到被测试类中。

当测试不依赖于 Spring 容器时,MockitoExtension 可以模拟外部依赖,专注于测试目标类的逻辑。在测试中,如果目标类依赖于外部服务或组件,可以通过 @Mock 注解模拟这些依赖,从而集中测试目标类的行为。

2、@Mock

用于创建模拟对象。被该注解标记的字段,在测试类初始化时,Mockito 会自动为其创建一个模拟实例,在测试中模拟对象可以替代真实对象,用于获取对象中的方法进行测试。

当你需要测试 A 类中的 a 方法,a 方法引用了 B 类的 b 方法。其中我们称 a 方法为要测的业务方法,b 方法称为 a 业务中所要使用的服务方法,例如调用数据库。@Mock 注解就添加在要引入的 B 类的上面。

即,测试方法中调用的服务就是需要模拟的对象。

3、@Spy

用于创建一个真实对象的部分模拟(Spy)对象。与 @Mock 不同,@Spy 创建的对象会保留真实对象的部分行为,只有被 stubbed(设定特定行为)的方法才会按照模拟的方式执行。相当于这个注解注入的类还能引用部分自身逻辑。

4、@InjectMocks

用于自动将标记为 @Mock@Spy 的依赖注入到被测试的类中。这样可以方便地为被测试类提供模拟的依赖对象,减少手动实例化和注入的代码。这个注解就是放在我们要测试的那个方法所在类的上面,即 A 方法的上面。

5、@Captor

该注解用于捕获传递给模拟对象方法的参数。这在需要验证方法调用时传递的参数是否符合预期,或者需要对传递的参数进行进一步操作时非常有用。

6、@BeforeEach / @BeforeAll

这两个注解就是用来在测试用例运行之前对定义的对象做一些操作的,例如赋值或初始化。

7、示例一

该示例中 MyRepository 就是类 B ,引用的服务类,getData 就是服务方法 b,MyService 就是类 A ,测试方法所在类,getNewData 就是方法 a,要测试的方法。

import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class) // 启用 Mockito 扩展
public class MyServiceTest {

    @Mock
    private MyRepository myRepository; // 模拟 MyRepository

    @InjectMocks
    private MyService myService; // 将模拟的 myRepository 注入到 MyService 中

	private User user;

	@BeforeEach
	void setUp(){
		// 用于初始化 Mockito 注解
		MockitoAnnotations.openMocks(this);
		user = new User("jack", 20, "男");
	}

    @Test
    void testServiceMethod() {
        // 模拟行为
        when(myRepository.getData(any(LocalDate.class))).thenReturn("Mocked Data");
        // 测试服务方法
        String result = myService.getNewData();
        // 验证结果
        assertEquals("Mocked Data", result);
    }

	// 这里@Spy注解创建了MathCalculator的Spy对象,add方法被模拟,返回固定值10,而不是真实的计算结果。

	@Spy
    private MathCalculator calculator = new MathCalculator();

    @Test
    public void testAdd() {
        // 模拟add方法的行为
        Mockito.when(calculator.add(2, 3)).thenReturn(10);

        int result = calculator.add(2, 3);
        assertEquals(10, result);
    }
    
    class MathCalculator {
    	public int add(int a, int b) {
        	return a + b;
    	}
	}

}

在这个例子中,@Mock 注解用于创建模拟对象,@InjectMocks 注解用于将模拟对象注入到被测试类中。如果测试需要 Spring 的上下文支持(例如依赖注入),则需要使用 @ExtendWith(SpringExtension.class)@SpringBootTest ,而不是 @ExtendWith(MockitoExtension.class)

8、示例二

该示例中@InjectMocks@Mock 创建的 productDao 注入到 OrderService 中,使 OrderService 在测试时能使用模拟的 ProductDao 行为。

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class OrderServiceTest {

    @Mock
    private ProductDao productDao;

    @InjectMocks
    private OrderService orderService;

    @Test
    public void testCalculateOrderTotal() {
        // 模拟productDao的getPrice方法
        Mockito.when(productDao.getPrice(1)).thenReturn(10.0);

        double total = orderService.calculateOrderTotal(1, 2);
        assertEquals(20.0, total);
    }
}

class OrderService {
    private ProductDao productDao;

    public OrderService(ProductDao productDao) {
        this.productDao = productDao;
    }

    public double calculateOrderTotal(int productId, int quantity) {
        double price = productDao.getPrice(productId);
        return price * quantity;
    }
}

class ProductDao {
    public double getPrice(int productId) {
        // 实际实现会从数据库等获取价格
        return 0;
    }
}

9、@Captor 示例

这里 @Captor 注解结合 ArgumentCaptor 捕获了传递给 emailSender.send 方法的第一个参数(邮箱地址),并验证其是否为预期值。

import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class EmailServiceTest {

    @Mock
    private EmailSender emailSender;

    @Captor
    private ArgumentCaptor<String> emailAddressCaptor;

    @Test
    public void testSendEmail() {
        EmailService emailService = new EmailService(emailSender);
        emailService.sendEmail("test@example.com", "Hello");

        Mockito.verify(emailSender).send(emailAddressCaptor.capture(), Mockito.anyString());
        assertEquals("test@example.com", emailAddressCaptor.getValue());
    }
    
}

class EmailService {

    private EmailSender emailSender;

    public EmailService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void sendEmail(String to, String content) {
        emailSender.send(to, content);
    }
    
}

class EmailSender {

    public void send(String to, String content) {
        // 实际发送邮件的逻辑
    }
    
}

四、注意事项

  • 避免过度模拟:虽然模拟可以隔离测试,但过度模拟可能导致测试失去意义,因为它与实际运行环境相差太大。
  • 维护测试的可读性:确保测试代码清晰易懂,避免过于复杂的模拟设置和验证逻辑。
  • 及时更新测试:当被测试类的接口或依赖发生变化时,及时更新相应的测试代码。

好了,通过以上介绍,你应该对 Mockito 单元测试有了较为清晰的了解,赶紧上手试试吧。

相关文章:

  • C语言表驱动法
  • 【SpringBoot苍穹外卖】debugDay02
  • 飞牛OS与昔映OS深度对比
  • debian和ubuntu安装python3.8并修改默认python版本
  • Renesas RH850 EEL库介绍
  • 自动化办公|xlwings 数据类型和转换
  • 炸裂:SpringAI内置DeepSeek啦!
  • 服务器被暴力破解的一次小记录
  • 基于A*算法与贝塞尔曲线的路径规划与可视化:从栅格地图到平滑路径生成
  • 动手学深度学习---深层神经网络
  • 如何评估云原生GenAI应用开发中的安全风险(下)
  • Python 依赖管理的革新——Poetry 深度解析
  • 简单记录一下自己对springboot过程的理解
  • zsh: command not found: conda
  • 香港服务器系统怎么查看端口是否开放?
  • jenkins自动化部署,环境搭建,应用部署
  • UNITY计算fps时应忽略掉time.timescale的影响
  • 本地部署DeepSeek摆脱服务器繁忙
  • Java的synchronized是怎么实现的?
  • 高级 Conda 使用:环境导出、共享与优化
  • 光大华夏:近代中国私立大学遥不可及的梦想
  • 深入贯彻中央八项规定精神学习教育中央第六指导组指导督导中国工商银行见面会召开
  • 黄晨光任中科院空间应用工程与技术中心党委书记、副主任
  • 黄仁勋:中国AI市场将达500亿美元,美国企业若无法参与是巨大损失
  • 金融监管总局:支持银行有序设立科技金融专门机构,推动研发机器人、低空飞行器等新兴领域的保险产品
  • 央行将增加3000亿元科技创新和技术改造再贷款额度