轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

    • 经验

    • JVM

    • 企业应用

      • Freemarker模板
      • Servlet与JSP指南
      • Java日志系统
      • Java JSON处理
      • Java XML处理
      • Java对象池技术
      • Java程序的单元测试(Junit)
      • Thymeleaf官方文档(中文版)
      • Mockito应用
        • 一、概述
          • 1. 什么是Mockito
          • 2. Mockito的优势
          • 3. Mockito的适用场景
        • 二、安装与配置
          • 1. 添加依赖
          • 1.1 Maven配置
          • 1.2 Gradle配置
        • 三、基础用法
          • 1. 创建Mock对象
          • 1.1 使用 @Mock 注解
          • 1.2 使用 Mockito.mock() 方法
          • 2. 设置行为
          • 2.1 when-thenReturn
          • 2.2 doReturn-when
          • 2.3 doThrow-when
          • 3. 验证行为
          • 3.1 verify()
          • 3.2 verifyNoInteractions()
          • 3.3 verifyZeroInteractions()
        • 四、进阶技巧
          • 1. Stubbing
          • 1.1 返回值
          • 1.2 异常抛出
          • 1.3 连续调用
          • 2. ArgumentMatchers
          • 2.1 任意参数
          • 2.2 特定参数
          • 2.3 参数捕获器
          • 3. Spies
          • 3.1 创建Spy
          • 3.2 使用Spy
          • 4. 自定义Answer
          • 4.1 使用 thenAnswer
          • 4.2 实现 Answer 接口
          • 5. 验证顺序
          • 5.1 使用 inOrder 验证顺序
          • 6. 重置Mock对象
          • 6.1 使用 reset 重置Mock对象
          • 7. Mock静态方法
          • 7.1 使用 MockedStatic 模拟静态方法
          • 7.2 解释
          • 7.3 使用限制
        • 五、实战案例
          • 1. 单元测试示例
          • 1.1 基础单元测试
          • 测试一个简单的Service
          • 测试DAO层
          • 1.2 模拟复杂的业务逻辑
          • 测试一个具有多个依赖的服务
          • 2. 整合Spring框架
          • 2.1 在Spring Boot中使用Mockito
          • 配置Spring Boot测试
          • 测试Controller
          • 2.2 使用@MockBean和@SpyBean
          • 使用@MockBean
          • 使用@SpyBean
        • 六、常见问题与解决方案
          • 1. 常见错误
          • 1.1 NullPointerException
          • 1.2 NoInteractionsWanted
          • 1.3 UnexpectedMethodCallException
          • 2. 解决方案
          • 2.1 确保Mock对象被初始化
          • 2.2 正确设置Stubbing
          • 2.3 检查方法调用顺序
        • 七、总结
          • 1. Mockito的核心概念回顾
          • 2. 最佳实践建议
      • Java中的空安全
  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 企业应用
轩辕李
2024-04-04
目录

Mockito应用

# 一、概述

# 1. 什么是Mockito

Mockito 是一个流行的 Java 单元测试框架,用于创建和配置 mock 对象。

它通过模拟对象的行为,帮助开发者编写更加可靠和可维护的单元测试。

Mockito 提供了简单易用的 API,使得测试代码更简洁和直观。

# 2. Mockito的优势

  • 简单易用:Mockito 的 API 设计非常直观,易于理解和使用。
  • 强大的功能:支持多种方式来设置 mock 对象的行为,包括返回值、异常抛出等。
  • 良好的社区支持:拥有活跃的社区和丰富的文档资源,容易找到问题的解决方案。
  • 集成性好:可以轻松地与各种构建工具(如 Maven 和 Gradle)和 IDE(如 IntelliJ IDEA 和 Eclipse)集成。
  • 性能优化:Mockito 的实现经过了性能优化,确保在大规模测试中也能保持高效。

# 3. Mockito的适用场景

  • 单元测试:Mockito 最常用于单元测试中,通过模拟依赖对象来隔离被测代码,从而更容易进行测试。
  • 复杂业务逻辑:当业务逻辑涉及多个服务或组件时,使用 Mockito 可以模拟这些服务或组件,使测试更加集中于被测功能。
  • 外部系统集成:在需要测试与外部系统的交互时,Mockito 可以模拟这些外部系统的行为,避免对外部系统的实际调用。
  • 多线程环境:虽然 Mockito 主要用于单线程测试,但在某些情况下也可以用于测试多线程环境中的行为。
  • Spring 应用程序:在 Spring 框架中,Mockito 可以与 @MockBean 和 @SpyBean 等注解结合使用,方便地进行 Spring 组件的测试。

# 二、安装与配置

# 1. 添加依赖

在使用 Mockito 之前,需要在项目的构建配置文件中添加相应的依赖。以下是 Maven 和 Gradle 的配置示例。

# 1.1 Maven配置

在 pom.xml 文件中添加以下依赖:

<dependencies>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-all</artifactId>
        <version>LATEST</version>
        <scope>test</scope>
    </dependency>
</dependencies>

# 1.2 Gradle配置

在 build.gradle 文件中添加以下依赖:

dependencies {
    testImplementation 'org.mockito:mockito-all:LATEST'
}

# 三、基础用法

# 1. 创建Mock对象

在 Mockito 中,创建 mock 对象有两种主要方式:使用 @Mock 注解和 Mockito.mock() 方法。

# 1.1 使用 @Mock 注解

@Mock 注解用于字段注入,通常与 @RunWith(MockitoJUnitRunner.class)(适用于Junit4) 或 @ExtendWith(MockitoExtension.class)(适用于Junit5) 一起使用。

以下是一个示例:

import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class)
public class MyTest {

    @Mock
    private MyService myService;

    @Test
    public void testMethod() {
        // 你可以直接使用 myService 进行测试
        when(myService.doSomething()).thenReturn("mocked result");
        String result = myService.doSomething();
        assertEquals("mocked result", result);
    }
}

# 1.2 使用 Mockito.mock() 方法

Mockito.mock() 方法用于手动创建 mock 对象。以下是一个示例:

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething()).thenReturn("mocked result");
        String result = myService.doSomething();
        assertEquals("mocked result", result);
    }
}

# 2. 设置行为

Mockito 提供了多种方法来设置 mock 对象的行为。以下是一些常用的方法。

# 2.1 when-thenReturn

when-thenReturn 是最常用的设置 mock 对象返回值的方法。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething()).thenReturn("mocked result");

        String result = myService.doSomething();
        assertEquals("mocked result", result);
    }
}

# 2.2 doReturn-when

doReturn-when 用于处理更复杂的情况,如返回值依赖于方法参数或需要模拟 void 方法的行为。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);

        // 模拟有参数的方法
        doReturn("mocked result").when(myService).doSomething("param");

        String result = myService.doSomething("param");
        assertEquals("mocked result", result);

        // 模拟 void 方法
        doNothing().when(myService).doSomethingVoid();

        myService.doSomethingVoid();
        verify(myService).doSomethingVoid();
    }
}

# 2.3 doThrow-when

doThrow-when 用于模拟方法抛出异常的情况。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);

        // 模拟方法抛出异常
        doThrow(new RuntimeException("Expected exception")).when(myService).doSomething();

        try {
            myService.doSomething();
            fail("Expected exception was not thrown");
        } catch (RuntimeException e) {
            assertEquals("Expected exception", e.getMessage());
        }
    }
}

# 3. 验证行为

验证 mock 对象的行为是确保测试覆盖的一个重要步骤。以下是一些常用的验证方法。

# 3.1 verify()

verify() 用于验证 mock 对象的方法是否被调用。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);

        myService.doSomething();

        // 验证方法被调用一次
        verify(myService, times(1)).doSomething();
    }
}

# 3.2 verifyNoInteractions()

verifyNoInteractions() 用于验证某个 mock 对象没有被调用任何方法。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);

        // 确保 myService 没有任何方法被调用
        verifyNoInteractions(myService);
    }
}

# 3.3 verifyZeroInteractions()

verifyZeroInteractions() 用于验证一个或多个 mock 对象没有任何方法被调用。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService1 = mock(MyService.class);
        MyService myService2 = mock(MyService.class);

        // 确保 myService1 和 myService2 都没有任何方法被调用
        verifyZeroInteractions(myService1, myService2);
    }
}

通过这些基本用法,你可以开始使用 Mockito 进行单元测试,并确保你的代码按预期工作。

# 四、进阶技巧

# 1. Stubbing

Stubbing 是 Mockito 中用于定义 mock 对象行为的一种方式。通过 stubbing,你可以指定方法的返回值、抛出异常或进行复杂的操作。

# 1.1 返回值

你可以使用 thenReturn 来设置方法的返回值。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething()).thenReturn("mocked result");

        String result = myService.doSomething();
        assertEquals("mocked result", result);
    }
}

# 1.2 异常抛出

使用 thenThrow 可以模拟方法抛出异常的情况。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething()).thenThrow(new RuntimeException("Expected exception"));

        try {
            myService.doSomething();
            fail("Expected exception was not thrown");
        } catch (RuntimeException e) {
            assertEquals("Expected exception", e.getMessage());
        }
    }
}

# 1.3 连续调用

可以使用 thenReturn 和 thenThrow 的组合来模拟多次调用时的不同行为。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething())
            .thenReturn("first call")
            .thenReturn("second call")
            .thenThrow(new RuntimeException("Expected exception"));

        assertEquals("first call", myService.doSomething());
        assertEquals("second call", myService.doSomething());

        try {
            myService.doSomething();
            fail("Expected exception was not thrown");
        } catch (RuntimeException e) {
            assertEquals("Expected exception", e.getMessage());
        }
    }
}

# 2. ArgumentMatchers

ArgumentMatchers 用于匹配传递给 mock 方法的参数。这在处理复杂参数时非常有用。

# 2.1 任意参数

使用 any() 匹配任意类型的参数。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething(any(String.class))).thenReturn("mocked result");

        String result = myService.doSomething("any string");
        assertEquals("mocked result", result);
    }
}

# 2.2 特定参数

使用 eq() 或 argThat() 匹配特定参数。

import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        when(myService.doSomething(eq("specific value"))).thenReturn("mocked result");

        String result = myService.doSomething("specific value");
        assertEquals("mocked result", result);

        // 使用自定义匹配器
        when(myService.doSomething(argThat(s -> s.length() > 5))).thenReturn("mocked result for long string");

        String longResult = myService.doSomething("long string");
        assertEquals("mocked result for long string", longResult);
    }
}

# 2.3 参数捕获器

使用 ArgumentCaptor 捕获传递给方法的参数,以便后续验证。

import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);
        myService.doSomething("capture this argument");

        ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
        verify(myService).doSomething(argumentCaptor.capture());

        String capturedArgument = argumentCaptor.getValue();
        assertEquals("capture this argument", capturedArgument);
    }
}

# 3. Spies

Spies 是部分模拟的对象,允许你在不完全模拟对象的情况下对其进行测试。

# 3.1 创建Spy

使用 spy() 方法创建 spy 对象。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService realObject = new MyServiceImpl();
        MyService spyObject = spy(realObject);

        // 调用实际方法
        String result = spyObject.doSomething();
        assertEquals("real result", result);

        // 模拟方法
        doReturn("mocked result").when(spyObject).doSomething();

        String mockedResult = spyObject.doSomething();
        assertEquals("mocked result", mockedResult);
    }
}

# 3.2 使用Spy

使用 spy 对象时,你可以选择性地模拟某些方法,而其他方法仍然调用真实实现。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService realObject = new MyServiceImpl();
        MyService spyObject = spy(realObject);

        // 调用实际方法
        String realResult = spyObject.doSomething();
        assertEquals("real result", realResult);

        // 模拟方法
        doReturn("mocked result").when(spyObject).doSomething();

        String mockedResult = spyObject.doSomething();
        assertEquals("mocked result", mockedResult);

        // 验证方法被调用
        verify(spyObject, times(2)).doSomething();
    }
}

# 4. 自定义Answer

当你需要更复杂的逻辑来控制方法的行为时,可以使用自定义 Answer。

# 4.1 使用 thenAnswer

使用 thenAnswer 来定义一个自定义的行为。

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);

        when(myService.doSomething(anyString()))
            .thenAnswer(invocation -> {
                String arg = invocation.getArgument(0);
                return "Processed: " + arg.toUpperCase();
            });

        String result = myService.doSomething("hello");
        assertEquals("Processed: HELLO", result);
    }
}

# 4.2 实现 Answer 接口

你也可以实现 org.mockito.stubbing.Answer 接口来定义更复杂的行为。

import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import static org.mockito.Mockito.*;

public class MyTest {

    @Test
    public void testMethod() {
        MyService myService = mock(MyService.class);

        Answer<String> customAnswer = invocation -> {
            String arg = invocation.getArgument(0);
            return "Processed: " + arg.toUpperCase();
        };

        when(myService.doSomething(anyString())).thenAnswer(customAnswer);

        String result = myService.doSomething("hello");
        assertEquals("Processed: HELLO", result);
    }
}

# 5. 验证顺序

在某些情况下,我们不仅需要验证某个方法是否被调用,还需要验证这些方法的调用顺序。Mockito 提供了 inOrder 方法来帮助我们验证方法调用的顺序。

# 5.1 使用 inOrder 验证顺序

假设我们有一个 OrderService,它需要依次调用 processPayment 和 saveOrder 方法。

public class OrderService {
    private PaymentService paymentService;
    private OrderRepository orderRepository;

    public OrderService(PaymentService paymentService, OrderRepository orderRepository) {
        this.paymentService = paymentService;
        this.orderRepository = orderRepository;
    }

    public boolean placeOrder(Order order) {
        if (paymentService.processPayment(order.getAmount())) {
            orderRepository.save(order);
            return true;
        }
        return false;
    }
}

编写测试类:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class OrderServiceTest {

    @Mock
    private PaymentService paymentService;

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

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

    @Test
    void testPlaceOrder_Success() {
        // Arrange
        Order order = new Order(1L, 100.0);
        when(paymentService.processPayment(order.getAmount())).thenReturn(true);

        // Act
        boolean result = orderService.placeOrder(order);

        // Assert
        assertTrue(result);

        InOrder inOrder = inOrder(paymentService, orderRepository);
        inOrder.verify(paymentService).processPayment(order.getAmount());
        inOrder.verify(orderRepository).save(order);
    }
}

# 6. 重置Mock对象

有时候我们需要在多个测试方法之间重置 mock 对象的状态,以确保每个测试方法之间的独立性。Mockito 提供了 reset 方法来实现这一点。

# 6.1 使用 reset 重置Mock对象

假设我们有一个 UserService,并且我们需要在两个测试方法之间重置 UserRepository 的状态。

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }

    public boolean deleteUserById(Long id) {
        return userRepository.deleteById(id);
    }
}

编写测试类:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

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

    @Test
    void testGetUserById() {
        // Arrange
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // Act
        User result = userService.getUserById(userId);

        // Assert
        assertNotNull(result);
        assertEquals("John Doe", result.getName());

        // 重置mock对象
        reset(userRepository);
    }

    @Test
    void testDeleteUserById() {
        // Arrange
        Long userId = 1L;
        when(userRepository.deleteById(userId)).thenReturn(true);

        // Act
        boolean result = userService.deleteUserById(userId);

        // Assert
        assertTrue(result);

        verify(userRepository).deleteById(userId);
    }
}

# 7. Mock静态方法

Mockito 从版本 3.4.0 开始引入了 MockedStatic 接口,使得模拟静态方法变得更加简单和直观。我们可以通过 Mockito.mockStatic 方法来创建一个 MockedStatic 对象,从而在测试中模拟静态方法的行为。

# 7.1 使用 MockedStatic 模拟静态方法

假设我们有一个 Utils 类,其中包含一个静态方法 getSystemTime。

public class Utils {
    public static long getSystemTime() {
        return System.currentTimeMillis();
    }
}

我们需要模拟这个静态方法的行为,并在单元测试中进行验证。

然后,编写测试类:

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class UtilsTest {

    @Test
    void testGetSystemTime() {
        // Arrange
        long mockTime = 1633072800000L; // 2021-10-01 00:00:00

        try (MockedStatic<Utils> utilsMock = Mockito.mockStatic(Utils.class)) {
            utilsMock.when(Utils::getSystemTime).thenReturn(mockTime);

            // Act
            long result = Utils.getSystemTime();

            // Assert
            assertEquals(mockTime, result);
        }
    }

    @Test
    void testMultipleCallsToGetSystemTime() {
        // Arrange
        long mockTime1 = 1633072800000L; // 2021-10-01 00:00:00
        long mockTime2 = 1633076400000L; // 2021-10-01 01:00:00

        try (MockedStatic<Utils> utilsMock = Mockito.mockStatic(Utils.class)) {
            utilsMock.when(Utils::getSystemTime)
                     .thenReturn(mockTime1)
                     .thenReturn(mockTime2);

            // Act
            long result1 = Utils.getSystemTime();
            long result2 = Utils.getSystemTime();

            // Assert
            assertEquals(mockTime1, result1);
            assertEquals(mockTime2, result2);
        }
    }

    @Test
    void testVerifyCallToGetSystemTime() {
        // Arrange
        long mockTime = 1633072800000L; // 2021-10-01 00:00:00

        try (MockedStatic<Utils> utilsMock = Mockito.mockStatic(Utils.class)) {
            utilsMock.when(Utils::getSystemTime).thenReturn(mockTime);

            // Act
            long result = Utils.getSystemTime();

            // Assert
            assertEquals(mockTime, result);
            utilsMock.verify(() -> Utils.getSystemTime());
        }
    }
}

# 7.2 解释

  1. MockedStatic<Utils>: 创建一个 MockedStatic 对象,用于模拟 Utils 类的静态方法。
  2. utilsMock.when(Utils::getSystemTime).thenReturn(mockTime): 定义当调用 Utils.getSystemTime 方法时返回 mockTime。
  3. try-with-resources: MockedStatic 实现了 AutoCloseable 接口,因此可以使用 try-with-resources 语句来自动关闭它,恢复原始的静态方法行为。
  4. utilsMock.verify(() -> Utils.getSystemTime()): 验证 Utils.getSystemTime 方法是否被调用。

# 7.3 使用限制

虽然 MockedStatic 提供了一种方便的方式来模拟静态方法,但它也有一些使用上的限制:

  1. 不能模拟 final 类:
  • MockedStatic 无法模拟标记为 final 的类中的静态方法。例如,java.lang.System 是一个 final 类,所以你不能直接使用 MockedStatic 来模拟 System.currentTimeMillis()。

  • 示例代码:

    // 这会抛出异常
    try (MockedStatic<System> systemMock = mockStatic(System.class)) {
        // 这里会抛出异常,因为 System 是 final 类
    }
    
  1. 不能模拟 final 方法:
  • 如果类中的某个静态方法被标记为 final,那么 MockedStatic 也无法模拟该方法。

  • 示例代码:

    public class FinalMethodExample {
        public static final long getSystemTime() {
            return System.currentTimeMillis();
        }
    }
    
    // 这会抛出异常
    try (MockedStatic<FinalMethodExample> exampleMock = mockStatic(FinalMethodExample.class)) {
        // 这里会抛出异常,因为 getSystemTime 是 final 方法
    }
    
  1. 需要使用 mockito-inline 依赖:
  • 要模拟静态方法,除了 mockito-core 之外,还需要添加 mockito-inline 依赖。这是因为 mockito-inline 提供了内联字节码操作的功能,使得能够修改静态方法的行为。

  • 示例依赖配置:

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

# 五、实战案例

# 1. 单元测试示例

# 1.1 基础单元测试

# 测试一个简单的Service

假设我们有一个简单的 UserService,它依赖于 UserRepository 来获取用户信息。我们可以使用 Mockito 来模拟 UserRepository 的行为。

// UserService.java
public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
}

编写测试类:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

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

    @Test
    void testGetUserById() {
        // Arrange
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // Act
        User result = userService.getUserById(userId);

        // Assert
        assertNotNull(result);
        assertEquals("John Doe", result.getName());
    }

    @Test
    void testGetUserById_NotFound() {
        // Arrange
        Long userId = 100L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // Act
        User result = userService.getUserById(userId);

        // Assert
        assertNull(result);
    }
}
# 测试DAO层

假设我们有一个 UserRepository 接口,并且我们需要测试它的实现。

// UserRepositoryImpl.java
@Repository
public class UserRepositoryImpl implements UserRepository {
    @Override
    public Optional<User> findById(Long id) {
        // 模拟数据库查询
        if (id == 1L) {
            return Optional.of(new User(1L, "John Doe"));
        }
        return Optional.empty();
    }
}

编写测试类:

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
public class UserRepositoryImplTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void testFindById_Found() {
        // Act
        Optional<User> result = userRepository.findById(1L);

        // Assert
        assertTrue(result.isPresent());
        assertEquals("John Doe", result.get().getName());
    }

    @Test
    void testFindById_NotFound() {
        // Act
        Optional<User> result = userRepository.findById(100L);

        // Assert
        assertFalse(result.isPresent());
    }
}

# 1.2 模拟复杂的业务逻辑

# 测试一个具有多个依赖的服务

假设我们有一个 OrderService,它依赖于 OrderRepository 和 PaymentService。

// OrderService.java
public class OrderService {
    private OrderRepository orderRepository;
    private PaymentService paymentService;

    public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    public boolean placeOrder(Order order) {
        if (!paymentService.processPayment(order.getAmount())) {
            return false;
        }
        orderRepository.save(order);
        return true;
    }
}

// OrderRepository.java
public interface OrderRepository extends JpaRepository<Order, Long> {
}

// PaymentService.java
public interface PaymentService {
    boolean processPayment(double amount);
}

编写测试类:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

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

    @Test
    void testPlaceOrder_Success() {
        // Arrange
        Order order = new Order(1L, 100.0);
        when(paymentService.processPayment(order.getAmount())).thenReturn(true);

        // Act
        boolean result = orderService.placeOrder(order);

        // Assert
        assertTrue(result);
        verify(orderRepository).save(order);
    }

    @Test
    void testPlaceOrder_PaymentFailed() {
        // Arrange
        Order order = new Order(1L, 100.0);
        when(paymentService.processPayment(order.getAmount())).thenReturn(false);

        // Act
        boolean result = orderService.placeOrder(order);

        // Assert
        assertFalse(result);
        verify(orderRepository, never()).save(order);
    }
}

# 2. 整合Spring框架

# 2.1 在Spring Boot中使用Mockito

# 配置Spring Boot测试

在 Spring Boot 项目中,我们可以使用 @SpringBootTest 注解来配置和运行集成测试。结合 Mockito 可以方便地进行依赖注入和模拟。

import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class ApplicationTests {
    // 测试代码
}
# 测试Controller

假设我们有一个 UserController,它依赖于 UserService。

// UserController.java
@RestController
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user != null) {
            return ResponseEntity.ok(user);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

编写测试类:

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void testGetUserById_Found() throws Exception {
        // Arrange
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        when(userService.getUserById(userId)).thenReturn(user);

        // Act & Assert
        mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("John Doe"));
    }

    @Test
    void testGetUserById_NotFound() throws Exception {
        // Arrange
        Long userId = 100L;
        when(userService.getUserById(userId)).thenReturn(null);

        // Act & Assert
        mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isNotFound());
    }
}

# 2.2 使用@MockBean和@SpyBean

# 使用@MockBean

@MockBean 用于在 Spring 上下文中创建并注册一个 mock 对象。这使得我们在测试中可以轻松地替换掉实际的 bean。

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(UserController.class)
public class UserControllerWithMockBeanTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void testGetUserById_Found() throws Exception {
        // Arrange
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        when(userService.getUserById(userId)).thenReturn(user);

        // Act & Assert
        mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("John Doe"));
    }

    @Test
    void testGetUserById_NotFound() throws Exception {
        // Arrange
        Long userId = 100L;
        when(userService.getUserById(userId)).thenReturn(null);

        // Act & Assert
        mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isNotFound());
    }
}
# 使用@SpyBean

@SpyBean 用于在 Spring 上下文中创建并注册一个 spy 对象。这允许我们在测试中部分替换实际的 bean 行为。

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(UserController.class)
public class UserControllerWithSpyBeanTest {

    @Autowired
    private MockMvc mockMvc;

    @SpyBean
    private UserService userService;

    @Test
    void testGetUserById_Found() throws Exception {
        // Arrange
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        doReturn(user).when(userService).getUserById(userId);

        // Act & Assert
        mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("John Doe"));
    }

    @Test
    void testGetUserById_NotFound() throws Exception {
        // Arrange
        Long userId = 100L;
        doReturn(null).when(userService).getUserById(userId);

        // Act & Assert
        mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isNotFound());
    }
}

# 六、常见问题与解决方案

# 1. 常见错误

# 1.1 NullPointerException

NullPointerException 是在使用 Mockito 进行单元测试时常见的错误之一。这种异常通常发生在以下几种情况:

  • 没有正确初始化 mock 对象。
  • 在调用方法时传入了 null 参数。

示例代码:

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

错误的测试代码:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @BeforeEach
    void setUp() {
        // 忘记初始化mock对象
    }

    @Test
    void testGetUserById() {
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        User result = userService.getUserById(userId);

        assertNotNull(result);
        assertEquals("John Doe", result.getName());
    }
}

在这个例子中,setUp 方法中忘记调用 MockitoAnnotations.openMocks(this),导致 userRepository 没有被初始化,从而引发 NullPointerException。

# 1.2 NoInteractionsWanted

NoInteractionsWanted 异常通常表示在测试过程中,某个 mock 对象被意外地调用了,而我们预期该对象不应该有任何交互。

示例代码:

public class OrderService {
    private PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public boolean placeOrder(Order order) {
        if (order.getAmount() > 0) {
            return paymentService.processPayment(order.getAmount());
        }
        return false;
    }
}

错误的测试代码:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class OrderServiceTest {

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

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

    @Test
    void testPlaceOrder_NoPayment() {
        Order order = new Order(1L, 0.0);
        boolean result = orderService.placeOrder(order);

        assertFalse(result);

        verifyNoInteractions(paymentService); // 这里期望paymentService没有任何交互
    }
}

在这个例子中,placeOrder 方法在 order.getAmount() 为 0 时不应该调用 paymentService.processPayment,但如果实际调用了,则会抛出 NoInteractionsWanted 异常。

# 1.3 UnexpectedMethodCallException

UnexpectedMethodCallException 表示在测试过程中,某个 mock 对象被调用的方法不在预期之内。通常是因为没有正确设置 stubbing 或者调用了未定义的方法。

示例代码:

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

错误的测试代码:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

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

    @Test
    void testGetUserById() {
        Long userId = 1L;
        User user = new User(userId, "John Doe");

        // 错误的stubbing
        when(userRepository.findByName("John Doe")).thenReturn(Optional.of(user));

        User result = userService.getUserById(userId);

        assertNotNull(result);
        assertEquals("John Doe", result.getName());
    }
}

在这个例子中,when(userRepository.findByName("John Doe")) 被错误地设置为返回值,而实际上应该设置 when(userRepository.findById(userId))。这会导致 UnexpectedMethodCallException。

# 2. 解决方案

# 2.1 确保Mock对象被初始化

在每个测试类的 setUp 方法中,确保所有 mock 对象都被正确初始化。

正确的测试代码:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

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

    @Test
    void testGetUserById() {
        Long userId = 1L;
        User user = new User(userId, "John Doe");
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        User result = userService.getUserById(userId);

        assertNotNull(result);
        assertEquals("John Doe", result.getName());
    }
}

# 2.2 正确设置Stubbing

确保为需要模拟的方法正确设置 stubbing,并且调用的方法和参数匹配。

正确的测试代码:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

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

    @Test
    void testGetUserById() {
        Long userId = 1L;
        User user = new User(userId, "John Doe");

        // 正确的stubbing
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        User result = userService.getUserById(userId);

        assertNotNull(result);
        assertEquals("John Doe", result.getName());
    }
}

# 2.3 检查方法调用顺序

如果需要验证方法的调用顺序,可以使用 inOrder 来确保方法按预期顺序被调用。

正确的测试代码:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class OrderServiceTest {

    @Mock
    private PaymentService paymentService;

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

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

    @Test
    void testPlaceOrder_Success() {
        Order order = new Order(1L, 100.0);
        when(paymentService.processPayment(order.getAmount())).thenReturn(true);

        boolean result = orderService.placeOrder(order);

        assertTrue(result);

        InOrder inOrder = inOrder(paymentService, orderRepository);
        inOrder.verify(paymentService).processPayment(order.getAmount());
        inOrder.verify(orderRepository).save(order);
    }
}

# 七、总结

# 1. Mockito的核心概念回顾

在本文中,我们深入探讨了 Mockito 的核心概念和功能,包括如何创建和使用 mock 对象、验证方法调用、设置 stubbing 以及一些进阶技巧。以下是 Mockito 的一些核心概念的简要回顾:

  • Mock 对象:模拟的对象,用于替代真实的依赖对象,以便进行单元测试。
  • Stubbing:为 mock 对象的方法提供预定义的返回值或行为。
  • Verification:验证 mock 对象的方法是否按预期被调用。
  • Partial Mocking:部分模拟,允许对某些方法进行模拟,而其他方法则调用真实实现。
  • Spy:监视实际对象的行为,并可以在需要时对其进行部分模拟。
  • Argument Matchers:用于匹配方法参数,以便更灵活地设置 stubbing 和 verification。
  • 顺序验证:验证方法调用的顺序,确保按预期顺序执行。
  • 重置 Mock 对象:在多个测试方法之间重置 mock 对象的状态。
  • Mock 静态方法:使用 PowerMock 等扩展库来模拟静态方法。

# 2. 最佳实践建议

为了确保您的单元测试更加可靠和易于维护,以下是一些 Mockito 使用的最佳实践建议:

  • 尽可能使用接口:使用接口作为依赖注入,这样可以更容易地进行模拟。
  • 最小化 Stubbing:只设置必要的 stubbing,避免过度配置。
  • 明确的期望:在测试中明确说明你的期望,例如哪些方法应该被调用,哪些不应该。
  • 命名清晰:为测试方法和测试类命名清晰,使其易于理解。
  • 使用注解:利用 @Mock 和 @InjectMocks 注解简化代码。
  • 单独测试每个功能:每个测试方法应测试一个特定的功能点,保持测试的独立性。
  • 验证方法调用:确保所有的方法调用都经过验证,以保证行为正确。
  • 处理异常情况:在测试中处理并验证异常情况,确保代码的健壮性。
  • 文档化:为复杂的测试逻辑添加注释,方便团队成员理解和维护。

祝你变得更强!

编辑 (opens new window)
上次更新: 2025/02/04
Thymeleaf官方文档(中文版)
Java中的空安全

← Thymeleaf官方文档(中文版) Java中的空安全→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式