Java/Spring中测试Mockito Spy教程

Mockito 以其对单元测试的变革性影响而闻名,是一个强大的模拟框架。它的主要功能是使开发人员能够制作和操作模拟对象。这些模拟对于隔离应用程序的特定组件进行测试至关重要,不受外部依赖项和交互的不可预测性的影响。这种战略隔离不仅是效率问题,也是问题所在。它对于获得快速、可靠的测试结果、确保每个组件在受控环境中正常运行至关重要。

什么是Mockito Spy
Mockito 皇冠上的宝石之一是Mockito Spy功能。虽然传统的模拟完全是接口或类的虚拟实现,但 Mockito 中的Spy则更加微妙。它弥合了真实对象行为和模拟的灵活性之间的差距,提供了真实性和控制的独特结合。

通过包装真实对象,Spy允许大多数操作照常执行,同时仍然提供拦截和更改特定方法调用、跟踪交互或验证行为的能力。

Mockito Spy 作为混合测试工具在 Mockito 框架中脱颖而出,融合了真实对象和模拟对象的特征。与标准模拟不同,标准模拟本质上是空白画布,允许完全控制和自定义,而 Mockito 中的Spy则环绕真实对象。这种包装技术使Spy能够推迟对对象实际方法的调用,除非显式存根。因此,Spy程序非常适合需要监视真实对象行为,同时仍保留覆盖特定方法行为以进行测试的能力的场景。

在处理遗留代码时,Spy活动特别有用,因为为复杂系统的各个方面创建模拟既不实用也不高效。它在真实对象的行为非常复杂的情况下也很出色,并且在模拟中复制它会过于麻烦。通过使用Spy,开发人员可以在很大程度上保持对象的自然行为完整,仅在必要时介入以更改或跟踪特定功能。

这使得 Mockito Spy 成为提高测试准确性和效率不可或缺的工具,特别是在处理遗留代码或复杂的类交互时,完全模拟是不切实际或不可能的。

在接下来的部分中,我们将深入探讨 Mockito Spy 的功能,阐明其在 Java 应用程序中构建高质量、可维护的测试方面的作用和重要性。

为 Mockito Spy 设置环境
要开始使用 Mockito Spy,正确设置 Java 项目环境至关重要。首先,确保您的项目包含 Mockito 库。如果您使用 Maven,请将以下依赖项添加到您的pom.xml文件中:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

对于 Gradle 用户,请将此行包含在您的build.gradle中:

testImplementation 'org.mockito:mockito-core:[latest-version]'
当前版本的Mockito可以在maven 存储库中找到。添加依赖项后,同步您的项目以确保 Mockito 库已下载并可供使用。

接下来,配置您的测试环境。Mockito 通常与JUnit一起使用,因此请确保在项目中设置了 JUnit。有了这些依赖项,您现在就可以开始编写利用 Mockito Spy 功能的测试,从而增强单元测试的范围和有效性。

使用 Mockito Spy 编写您的第一个测试
使用 Mockito Spy 编写第一个测试涉及三个关键步骤:创建Spy、验证交互和存根方法调用。让我们通过 Java 代码示例来完成每个步骤。

假设我们有一个要测试的简单计算器类:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public int subtract(int a, int b) {
        return a - b;
    }
}

第 1 步:创建一个Spy
首先,我们需要创建Calculator类的Spy。这可以使用Mockito 提供的Spy方法来完成。

import org.mockito.Mockito;
import static org.junit.Assert.*;
import org.junit.Test;

public class CalculatorTest {

    @Test
    public void testAddition() {
        Calculator calculator = new Calculator();
        Calculator spyCalculator = Mockito.spy(calculator);

        // Rest of the test
    }
}

在此示例中,spyCalculator是实际计算器对象的Spy。

第 2 步:验证交互
接下来,我们验证是否使用特定参数调用了方法。这是使用验证方法完成的。

@Test
public void testAddition() {
    Calculator calculator = new Calculator();
    Calculator spyCalculator = Mockito.spy(calculator);

    spyCalculator.add(10, 20);

    Mockito.verify(spyCalculator).add(10, 20);

    // Rest of the test
}

在这里,我们验证是否使用参数10和20调用了Spy对象的add方法。

第 3 步:存根方法调用
最后,您可能想要更改Spy中方法的行为。这是通过存根方法调用来实现的。

@Test
public void testSubtraction() {
    Calculator calculator = new Calculator();
    Calculator spyCalculator = Mockito.spy(calculator);

    Mockito.doReturn(5).when(spyCalculator).subtract(10, 5);
    
    int result = spyCalculator.subtract(10, 5);

    assertEquals(5, result);
    Mockito.verify(spyCalculator).subtract(10, 5);
}

在此示例中,我们对减法进行了存根处理。即使实际的实现会返回5,我们也可以对其进行存根以返回任何其他值。这展示了我们如何控制Spy对象中特定方法的行为。

通过执行这些步骤,您可以创建Spy、验证其交互以及存根方法调用,从而为您的单元测试提供更大的灵活性和控制力。

Spring 框架和 Mockito @Spy
将 Mockito 的@Spy注释与最新的Spring 框架和JUnit 集成提供了一种强大的测试 Spring 组件的方法。它允许您部分模拟 Spring bean,同时保持 Spring 的上下文设置和依赖项注入功能。以下是如何在 Java 应用程序中使用AssertJ进行断言来实现这一点,它提供了更流畅和直观的断言方法。

设置依赖关系
对于Maven,请确保您的pom.xml文件中有必要的依赖项。您需要最新的 Spring Boot Starter Test,其中包括对 JUnit 和 Mockito 以及 AssertJ 的支持:

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

对于Gradle ,您可以在build.gradle文件中添加必要的依赖项,以包含最新的 Spring Boot Starter Test、Mockito 和 AssertJ。这是等效的 Gradle 导入:

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.2'
}

这将为您的 Gradle 项目设置必要的依赖项,以便将 Mockito @Spy与 Spring Framework 和 JUnit 结合使用,以及用于断言的 AssertJ。

示例场景:测试 Spring 服务
假设您有一个简单的 Spring 服务:

@Service
public class UserService {
    public String getUserDetails(String userId) {
        // Logic to retrieve user details
        return
"User Details for " + userId;
    }
}

使用 @Spy 编写测试
使用Spring的@SpringBootTest创建一个测试类来加载应用程序上下文。使用 Mockito 的@SpyBean将服务的Spy注入到 Spring 上下文中。

import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.mockito.SpyBean;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class UserServiceTest {

    @SpyBean
    private UserService userService;

    @Test
    public void getUserDetailsTest() {
        String userId = "123";
        String expected =
"User Details for 123";

       
// Call the real method
        String actual = userService.getUserDetails(userId);

       
// Assert using AssertJ
        assertThat(actual).isEqualTo(expected);
    }
}

在此测试中,UserService受到监视,这意味着除非显式存根,否则将调用其真正的方法。AssertJ 提供了一种更易读的方式来断言预期结果。

测试中的重写行为
您还可以覆盖spy中的某些行为以进行更有针对性的测试:

import org.mockito.Mockito;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.mockito.SpyBean;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class UserServiceTest {

    @SpyBean
    private UserService userService;

    @Test
    public void getUserDetailsTestWithStubbing() {
        String userId = "123";
        String stubbedResult =
"Stubbed User Details";

       
// Stubbing a method
        Mockito.doReturn(stubbedResult)
                .when(userService)
                .getUserDetails(userId);

        String actual = userService.getUserDetails(userId);

       
// Assert using AssertJ
        assertThat(actual).isEqualTo(stubbedResult);
    }
}

在此示例中,getUserDetails(userId)被存根以返回预定字符串。当您需要隔离某些行为而不改变 bean 的整个功能时,这非常有用。

真实场景
让我们考虑一个更真实的场景,其中涉及一个服务,该服务的方法依赖于外部 API数据库。在这种情况下,使用Spy来覆盖特定方法的行为对于测试特别有利,因为它允许您避免对外部系统进行实际调用,这可能是耗时的、不可靠的或有副作用的。

想象一下,您有一个UserService,它具有检索详细用户信息的方法,其中包括调用外部电子邮件服务来获取用户的电子邮件地址。在测试环境中,您可能希望避免调用此外部服务。

这是一个例子:

public class UserService {
    private EmailService emailService;

    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public UserDetails getUserDetails(String userId) {
        String email = emailService.getEmailForUser(userId);
        // Additional logic to retrieve user details
        return new UserDetails(userId, email);
    }
}

public class EmailService {
    public String getEmailForUser(String userId) {
       
// Makes an external API call to get email
        return
"user@example.com"; // Simplified for example
    }
}

在测试中,您可以使用Spy来覆盖EmailService的getEmailForUser(userId)方法,以避免进行外部调用:

import org.mockito.Mockito;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.mockito.SpyBean;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class UserServiceTest {

    @SpyBean
    private EmailService emailService;

    @Test
    public void getUserDetailsWithoutExternalCall() {
        String userId = "123";
        String stubbedEmail =
"stubbed-email@example.com";

       
// Stubbing the external call
        Mockito.doReturn(stubbedEmail)
                .when(emailService)
                .getEmailForUser(userId);

        UserService userService = new UserService(emailService);
        UserDetails userDetails = userService.getUserDetails(userId);

       
// Assert using AssertJ
        assertThat(userDetails.getEmail()).isEqualTo(stubbedEmail);
    }
}

在此修改后的示例中,UserServiceTest使用Spy来存根EmailService的getEmailForUser()方法。这样,测试不会进行实际的外部调用,而是使用存根电子邮件地址,从而使测试更加可靠和更快,因为它不依赖于外部服务的可用性或行为。

此外,利用 Mockito 的@SpyBean以及Spring Boot和 AssertJ 可以提供灵活监视Spring 管理的 bean和流畅断言的清晰度的双重优势,从而增强测试的稳健性和可读性。

Mockito Spy 的高级用途
Mockito Spy 提供了一系列高级功能,在复杂的测试场景中特别有用。让我们探讨其中的一些功能,包括监视真实对象、部分模拟以及使用 Java 代码示例支持的遗留代码进行监视。

监视真实对象
使用 Mockito,您可以创建真实对象的spy,其中某些方法具有真实行为,而其他方法则被覆盖(存根)。当您想要测试实际对象但需要更改某些行为以适应您的测试场景时,这特别有用。

考虑一个与数据库交互的UserManager类:

public class UserManager {
    public boolean isValidUser(String username) {
        // Connects to the database and checks if the user is valid
        return true;
// Simplified for the example
    }

    public void updateUser(String username) {
       
// Updates user in the database
    }
}

我们可以监视这个真实的对象并重写isValidUser方法:

import org.mockito.Mockito;
import org.junit.Test;

public class UserManagerTest {

    @Test
    public void testUpdateUser() {
        UserManager userManager = new UserManager();
        UserManager spyUserManager = Mockito.spy(userManager);

        // Stubbing the method
        Mockito.doReturn(false).when(spyUserManager).isValidUser(
"testUser");

       
// 'isValidUser' will use the stubbed value, 'updateUser' will execute real code
        if(spyUserManager.isValidUser(
"testUser")) {
            spyUserManager.updateUser(
"testUser");
        }

       
// Verify 'updateUser' was not called due to the stubbed method
        Mockito.verify(spyUserManager, Mockito.never()).updateUser(
"testUser");
    }
}

部分模拟
部分模拟是 Mockito Spy 的另一个强大功能。这允许您仅模拟对象的某些方法,而让其余方法保留其真实行为。当使用复杂和简单方法的混合来测试类时,这非常有用,其中无需模拟所有内容。

public class PaymentProcessor {
    public boolean processPayment(double amount) {
        // Complex payment processing logic
        return true;
    }

    public void notifyUser() {
       
// Simple notification logic
    }
}

以下是部分模拟PaymentProcessor的方法:

import org.mockito.Mockito;
import org.junit.Test;
import static org.junit.Assert.*;

public class PaymentProcessorTest {

    @Test
    public void testProcessPayment() {
        PaymentProcessor paymentProcessor = new PaymentProcessor();
        PaymentProcessor spyPaymentProcessor = Mockito.spy(paymentProcessor);

        // Stubbing processPayment method
        Mockito.doReturn(false)
                .when(spyPaymentProcessor)
                .processPayment(100.0);

        assertFalse(spyPaymentProcessor.processPayment(100.0));
        spyPaymentProcessor.notifyUser();

       
// Verify that 'notifyUser' method ran with real behavior
        Mockito.verify(spyPaymentProcessor).notifyUser();
    }
}

将 Spies 与旧代码一起使用
Mockito Spy 在处理遗留代码时特别有用。通常,此类代码库在设计时并未考虑到测试,因此很难使用传统模拟进行测试。Spy可以帮助您使用真实的代码,但覆盖难以测试的特定部分,例如外部依赖项。

假设有一个遗留的OrderProcessor类:

public class OrderProcessor {
    public void processOrder(String orderId) {
        // Complex logic with external dependencies
    }

    public void logOrder(String orderId) {
       
// Logging logic
    }
}

我们可以使用SPY来测试OrderProcessor:

import org.mockito.Mockito;
import org.junit.Test;

public class OrderProcessorTest {

    @Test
    public void testProcessOrder() {
        OrderProcessor orderProcessor = new OrderProcessor();
        OrderProcessor spyOrderProcessor = Mockito.spy(orderProcessor);

        // Stubbing external dependency part
        Mockito.doNothing()
                .when(spyOrderProcessor)
                .logOrder(
"123");

        spyOrderProcessor.processOrder(
"123");

       
// Verify that the 'logOrder' method was called
        Mockito.verify(spyOrderProcessor).logOrder(
"123");
    }
}

通过利用 Mockito Spy 的这些高级功能,您可以更轻松、更精确地处理各种复杂的测试场景,使您的测试更加稳健和可靠。

最佳实践和常见陷阱
在本节中,我们将探讨使用 Mockito Spy 的基本准则和典型失误,为有效且无错误的测试实践提供路线图。

使用 Mockito Spy 的最佳实践
深入研究在测试过程中最大限度提高 Mockito Spy 的效率和准确性的关键策略。

  • 谨慎使用Spy:应谨慎使用Spy。尽可能使用标准模拟,因为它们会导致更简单且更易于维护的测试。主要在处理遗留代码或测试依赖于真实对象的行为时使用Spy。
  • 明确定义存根:在Spy中存根方法时,明确定义要覆盖的行为。避免不必要的存根,因为它可能导致混乱和脆弱的测试。
  • 小心验证:验证要精确。过度验证可能会使您的测试变得脆弱,而验证不足可能会导致错过错误。专注于验证与测试目的直接相关的行为。
  • 保持测试重点:每个测试都应该验证单个行为或功能。这使得测试更容易理解和维护。
  • 记录您的意图:评论您的测试代码,尤其是当使用Spy背后的原因并不明显时。这有助于其他人理解为什么Spy比模拟更重要。

要避免的常见陷阱
让我们来看看使用 Mockito Spy 时要避免的一些常见错误,以确保测试更顺利、更可靠。

  • 过度使用Spy:过度依赖Spy可能会导致测试更加复杂且难以维护。它还可以掩盖代码设计问题。
  • 存根一切:存根Spy中的每一个方法都违背了Spy的目的。如果您发现自己这样做,请考虑模拟是否更合适。
  • 忽略默认行为:使用Spy时,请记住它们默认使用真实的方法。忽略这一点可能会导致测试中出现意想不到的副作用。
  • 不隔离测试:确保您的测试是隔离的并且不依赖于其他测试创建的状态。这在使用Spy时尤其重要,因为它们与真实的物体一起工作。

遵循这些最佳实践并避免常见陷阱,您可以有效地利用 Mockito Spy,从而实现更干净、更可靠且可维护的测试。

Mockito Spy 与常规 Mocks:何时使用
Mockito 提供了两种创建测试替身的主要方法:Spies 和 Mocks。了解何时使用每种方法对于编写有效的测试至关重要。让我们在 Java 代码示例的支持下探讨它们的差异和适当的用例。

常规模拟
Mockito 中的模拟是完全模拟的对象,不保留真实对象的任何行为。它们最好在以下情况下使用:

您需要测试对象之间的交互。
该对象具有您要避免的外部依赖项,例如数据库或网络调用。
您正在隔离测试,不需要对象的任何实际实现。
考虑一个依赖于CreditCardProcessor 的PaymentService类:

public class PaymentService {
    private CreditCardProcessor processor;

    public PaymentService(CreditCardProcessor processor) {
        this.processor = processor;
    }

    public void processPayment(String creditCardNumber, double amount) {
        // Process payment using CreditCardProcessor
    }
}

测试PaymentService时,您可以模拟CreditCardProcessor:

import org.mockito.Mock;
import org.mockito.Mockito;
import org.junit.Before;
import org.junit.Test;

public class PaymentServiceTest {

    @Mock
    private CreditCardProcessor mockProcessor;

    private PaymentService paymentService;

    @Before
    public void setUp() {
        mockProcessor = Mockito.mock(CreditCardProcessor.class);
        paymentService = new PaymentService(mockProcessor);
    }

    @Test
    public void testProcessPayment() {
        paymentService.processPayment("123456789", 100.0);
        Mockito.verify(mockProcessor).process(
"123456789", 100.0);
    }
}

Mockito Spy
另一方面,Mockito Spy是部分模拟的对象,其中某些方法保留了真实的行为。Spy在以下情况下是理想的选择:

  •  
  • 您需要测试真实对象的大部分行为。
  • 您只想覆盖或跟踪特定方法。
  • 您正在处理难以重构以进行测试的遗留代码。

例如,考虑需要部分测试的NotificationService :

public class NotificationService {
    public void sendEmail(String message) {
        // Send an email
    }

    public void logNotification(String message) {
       
// Log the notification
    }
}

您可以使用Spy来测试NotificationService:

import org.mockito.Mockito;
import org.junit.Test;

public class NotificationServiceTest {

    @Test
    public void testSendEmail() {
        NotificationService notificationService = new NotificationService();
        NotificationService spyService = Mockito.spy(notificationService);

        Mockito.doNothing().when(spyService).logNotification("Test Message");

        spyService.sendEmail(
"Test Message");
        spyService.logNotification(
"Test Message");

        Mockito.verify(spyService).sendEmail(
"Test Message");
        Mockito.verify(spyService).logNotification(
"Test Message");
    }
}

在模拟和Spy之间进行选择

  • 当您想要完全模拟对象及其交互时,请使用模拟。
  • 当您需要对象的真实行为但想要覆盖或监视特定方法时,请选择Spy。

了解 Mockito 的模拟和Spy的细微差别可以让您编写既有效又适合您的测试场景的测试。

结论
在本文中,我们深入研究了在软件测试中使用 Mockito Spy 的实用性和优势。Mockito Spy 作为一款多功能工具脱颖而出,提供了真实对象行为和模拟功能的混合。这使得它在完全模拟不切实际的情况下特别有价值,例如处理遗留代码或需要保留真实对象的特定行为时。

我们观察到,Mockito Spy,特别是与 Spring 框架集成时,是一个用于部分模拟和监视真实对象的强大工具,在复杂的测试场景中表现出明显的优势。它能够精确验证交互并战略性地存根特定方法调用,从而实现更加受控和准确的测试过程。利用 Spring 的功能,例如@SpyBean和@MockBean,进一步增强了这种精度,使 Mockito Spy 能够与 Spring 强大的依赖管理和配置功能无缝结合。这种组合不仅提高了测试准确性,还为测试环境带来了新的效率和有效性水平。

然而,重要的是要记住,应该明智地使用 Mockito Spy。常规模拟通常更适合完全隔离和简单性,而Spy最适合需要混合真实行为和模拟行为的场景。