Mockito 中的间谍 (Spy)

JavaJavaBeginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

Mockito 是一个强大的 Java 框架,用于对 Java 应用程序进行模拟 (mocking)单元测试 (unit testing)。在这个实验中,我们将探讨 Mockito 中间谍 (spy) 的概念。间谍 (spy) 是一种独特的测试工具,它作为部分模拟对象发挥作用 —— 它会跟踪与对象的交互,同时仍然允许调用真实的方法。

这种双重行为使得间谍 (spy) 在我们想要验证与对象的交互,但仍需要执行真实功能时特别有用。在整个实验过程中,我们将学习如何使用不同的方法创建间谍 (spy),了解如何对间谍 (spy) 的行为进行存根 (stub) 处理,并将间谍 (spy) 与模拟对象 (mock) 进行比较,以突出它们的关键区别。

创建 Java 项目并理解 Mockito 间谍 (Spy)

在这一步中,我们将搭建项目结构,并了解 Mockito 中间谍 (Spy) 的概念。间谍 (Spy) 允许我们跟踪真实对象上的方法调用,同时仍然使用它们的实际实现。

创建 Java 文件

首先,让我们为这个实验创建主 Java 文件。在 WebIDE 中,在 ~/project 目录下创建一个名为 MockitoSpyDemo.java 的新文件。

创建新文件

添加以下代码以导入必要的依赖项:

import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;

public class MockitoSpyDemo {

    public static void main(String[] args) {
        System.out.println("This is a Mockito Spy demonstration.");
        System.out.println("To run the tests, you need to use JUnit.");
    }
}

理解 Mockito 间谍 (Spy)

Mockito 中的间谍 (Spy) 是对真实对象的包装,它允许你:

  1. 跟踪与对象的所有交互(类似于模拟对象 (mock))
  2. 执行对象的真实方法(与模拟对象 (mock) 不同)

可以将间谍 (Spy) 视为一个观察者,它观察对象的行为,但除非特别编程,否则不会干扰其正常行为。

让我们添加第一个测试方法来演示间谍 (Spy) 的基本用法。将以下方法添加到你的 MockitoSpyDemo 类中:

@Test
public void basicSpyExample() {
    // Create a list and spy on it
    List<String> realList = new ArrayList<>();
    List<String> spyList = Mockito.spy(realList);

    // Using the spy to add an item (the real method is called)
    spyList.add("Hello");

    // Verify the interaction happened
    Mockito.verify(spyList).add("Hello");

    // The real method was called, so the list actually has the item
    assertEquals(1, spyList.size());
    assertEquals("Hello", spyList.get(0));

    System.out.println("Basic Spy Example - Spy List Content: " + spyList);
    System.out.println("Basic Spy Example - Spy List Size: " + spyList.size());
}

让我们编译并运行代码,看看是否能正常工作:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java
java -cp .:lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo
运行代码

你应该会看到以下输出:

This is a Mockito Spy demonstration.
To run the tests, you need to use JUnit.

我们的主方法运行正常。不过,要运行测试,我们需要使用 JUnit。在这个实验中,我们不会设置复杂的 JUnit 运行器,而是专注于理解代码结构和概念。

在下一步中,我们将探索在 Mockito 中创建间谍 (Spy) 的不同方法。

在 Mockito 中创建间谍 (Spy) 的不同方法

在这一步中,我们将探索在 Mockito 中创建间谍 (Spy) 的不同方法。我们将学习如何使用 Mockito.spy() 方法和 @Spy 注解来创建间谍 (Spy)。

使用 Mockito.spy() 方法

创建间谍 (Spy) 最直接的方法是使用 Mockito.spy() 方法。让我们添加一个测试方法来演示这种方法:

@Test
public void testSpyWithMockitoSpyMethod() {
    // Create an ArrayList and a spy of it
    ArrayList<Integer> realList = new ArrayList<>();
    ArrayList<Integer> spyList = Mockito.spy(realList);

    // Adding elements to the spy (real methods are called)
    spyList.add(5);
    spyList.add(10);
    spyList.add(15);

    // Verifying interactions
    Mockito.verify(spyList).add(5);
    Mockito.verify(spyList).add(10);
    Mockito.verify(spyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, spyList.size());

    System.out.println("Spy Method Example - Spy List Content: " + spyList);
    System.out.println("Spy Method Example - Spy List Size: " + spyList.size());
}

使用 @Spy 注解

另一种创建间谍 (Spy) 的方法是使用 @Spy 注解。当你想自动初始化间谍 (Spy) 时,这种方法特别有用。将以下代码添加到你的 MockitoSpyDemo 类中:

// Declare a spy using annotation
@Spy
ArrayList<Integer> annotationSpyList = new ArrayList<>();

@Before
public void initSpies() {
    // Initialize mocks and spies with annotations
    MockitoAnnotations.initMocks(this);
}

@Test
public void testSpyWithAnnotation() {
    // Adding elements to the spy
    annotationSpyList.add(5);
    annotationSpyList.add(10);
    annotationSpyList.add(15);

    // Verifying interactions
    Mockito.verify(annotationSpyList).add(5);
    Mockito.verify(annotationSpyList).add(10);
    Mockito.verify(annotationSpyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, annotationSpyList.size());

    System.out.println("Annotation Spy Example - Spy List Content: " + annotationSpyList);
    System.out.println("Annotation Spy Example - Spy List Size: " + annotationSpyList.size());
}

理解不同方法之间的区别

这两种方法都可以创建间谍 (Spy),但它们有不同的使用场景:

  1. Mockito.spy() 更灵活,可以在你的测试方法中的任何地方使用。
  2. @Spy 注解在你有多个间谍 (Spy) 并且想一次性初始化它们时更方便。

MockitoAnnotations.initMocks(this) 方法负责初始化所有带有注解的字段。在实际项目中,你可能会使用 @RunWith(MockitoJUnitRunner.class) 来自动完成这个初始化过程。

让我们编译更新后的代码:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

代码应该可以无错误地编译。我们的测试方法现在已经准备好了,但如前所述,我们需要一个 JUnit 运行器来自动执行它们。

在下一步中,我们将学习如何对间谍 (Spy) 进行存根 (stub) 处理,以覆盖它们的默认行为。

对间谍 (Spy) 进行存根 (Stubbing) 处理和异常处理

在这一步中,你将学习如何对间谍 (Spy) 进行存根 (Stubbing) 处理以覆盖其默认行为,以及如何处理使用 Mockito 时可能出现的异常。

对间谍 (Spy) 进行存根 (Stubbing) 处理

间谍 (Spy) 默认使用真实对象的方法,但有时为了测试目的,你需要改变这种行为。这被称为对间谍 (Spy) 进行“存根 (Stubbing)”处理。下面添加一个测试方法来演示如何对间谍 (Spy) 进行存根 (Stubbing) 处理:

@Test
public void testStubbingSpy() {
    // Create a spy of ArrayList
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add an element
    spyList.add(5);

    // Verify the element was added
    Mockito.verify(spyList).add(5);

    // By default, contains() should return true for element 5
    assertTrue(spyList.contains(5));
    System.out.println("Stubbing Example - Default behavior: spyList.contains(5) = " + spyList.contains(5));

    // Stub the contains() method to always return false for the value 5
    Mockito.doReturn(false).when(spyList).contains(5);

    // Now contains() should return false, even though the element is in the list
    assertFalse(spyList.contains(5));
    System.out.println("Stubbing Example - After stubbing: spyList.contains(5) = " + spyList.contains(5));

    // The element is still in the list (stubbing didn't change the list content)
    assertEquals(1, spyList.size());
    assertEquals(Integer.valueOf(5), spyList.get(0));
    System.out.println("Stubbing Example - Spy List Content: " + spyList);
}

关于存根 (Stubbing) 的重要注意事项

在对间谍 (Spy) 的方法进行存根 (Stubbing) 处理时,建议使用 doReturn()doThrow()doAnswer() 等方法,而不是 when()。这是因为对于间谍 (Spy),when().thenReturn() 语法在存根 (Stubbing) 过程中可能会调用真实方法,从而导致意外的副作用。

例如:

// This might call the real get() method, causing issues if index 10 doesn't exist
Mockito.when(spyList.get(10)).thenReturn(99); // May throw IndexOutOfBoundsException

// This is the correct way to stub a spy
Mockito.doReturn(99).when(spyList).get(10); // Safe, doesn't call the real method

处理 NotAMockException

如果你尝试在普通对象(不是模拟对象 (Mock) 或间谍 (Spy))上使用 Mockito 的验证方法,就会抛出 NotAMockException。下面添加一个测试方法来演示这个异常:

@Test
public void testNotAMockException() {
    try {
        // Create a regular ArrayList (not a mock or spy)
        ArrayList<Integer> regularList = new ArrayList<>();
        regularList.add(5);

        // Try to verify an interaction on a regular object
        Mockito.verify(regularList).add(5);

        fail("Expected NotAMockException was not thrown");
    } catch (NotAMockException e) {
        // Expected exception
        System.out.println("NotAMockException Example - Caught expected exception: " + e.getMessage());
        assertTrue(e.getMessage().contains("Argument passed to verify() is not a mock"));
    }
}

现在编译更新后的代码:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

代码应该能无错误地编译。在这一步中,你学习了如何对间谍 (Spy) 的方法进行存根 (Stubbing) 处理以覆盖其默认行为,以及如何处理在普通对象上使用验证方法时出现的 NotAMockException

下一步,你将比较模拟对象 (Mock) 和间谍 (Spy),以了解它们的关键区别。

模拟对象 (Mock) 与间谍 (Spy):理解差异

在这一步中,我们将直接比较模拟对象 (Mock) 和间谍 (Spy),以了解它们的关键区别。这种比较将帮助你根据测试需求选择合适的工具。

创建对比测试

让我们添加一个测试方法,来展示模拟对象 (Mock) 和间谍 (Spy) 之间的差异:

@Test
public void testMockVsSpyDifference() {
    // Create a mock and a spy of ArrayList
    ArrayList<Integer> mockList = Mockito.mock(ArrayList.class);
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add elements to both
    mockList.add(1);
    spyList.add(1);

    // Verify interactions (both will pass)
    Mockito.verify(mockList).add(1);
    Mockito.verify(spyList).add(1);

    // Check the size of both lists
    System.out.println("Mock vs Spy - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy - Spy List Size: " + spyList.size());

    // Mock returns default values (0 for size) unless stubbed
    assertEquals(0, mockList.size());

    // Spy uses real method implementation, so size is 1
    assertEquals(1, spyList.size());

    // Stub both to return size 100
    Mockito.when(mockList.size()).thenReturn(100);
    Mockito.when(spyList.size()).thenReturn(100);

    // Both should now return 100 for size
    assertEquals(100, mockList.size());
    assertEquals(100, spyList.size());

    System.out.println("Mock vs Spy (After Stubbing) - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy (After Stubbing) - Spy List Size: " + spyList.size());
}

理解关键差异

下面总结一下 Mockito 中模拟对象 (Mock) 和间谍 (Spy) 的关键区别:

  1. 方法行为

    • 模拟对象 (Mock):除非进行存根 (Stubbing) 处理,否则所有方法都返回默认值(null、0、false)。
    • 间谍 (Spy):除非进行存根 (Stubbing) 处理,否则方法调用真实的实现。
  2. 对象状态

    • 模拟对象 (Mock):操作不会改变对象的状态(例如,向模拟列表中添加元素实际上不会添加任何内容)。
    • 间谍 (Spy):操作会像真实对象一样改变对象的状态。
  3. 创建方式

    • 模拟对象 (Mock):从类创建,而不是从实例创建(Mockito.mock(ArrayList.class))。
    • 间谍 (Spy):通常从实例创建(Mockito.spy(new ArrayList<>()))。
  4. 使用场景

    • 模拟对象 (Mock):当你想完全控制对象的行为,且不需要使用真实方法时。
    • 间谍 (Spy):当你想使用真实方法,但只需要覆盖某些行为时。

实际示例:何时使用间谍 (Spy)

让我们再添加一个测试,展示间谍 (Spy) 的一个常见使用场景——对复杂对象进行部分存根 (Stubbing) 处理:

@Test
public void testWhenToUseSpy() {
    // Create a complex data object that we want to partially mock
    StringBuilder builder = new StringBuilder();
    StringBuilder spyBuilder = Mockito.spy(builder);

    // Use the real append method
    spyBuilder.append("Hello");
    spyBuilder.append(" World");

    // Verify the real methods were called
    Mockito.verify(spyBuilder).append("Hello");
    Mockito.verify(spyBuilder).append(" World");

    // The real methods modified the state
    assertEquals("Hello World", spyBuilder.toString());
    System.out.println("When to Use Spy - Content: " + spyBuilder.toString());

    // Stub the toString() method to return something else
    Mockito.when(spyBuilder.toString()).thenReturn("Stubbed String");

    // Now toString() returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
    System.out.println("When to Use Spy - After Stubbing toString(): " + spyBuilder.toString());

    // But other methods still work normally
    spyBuilder.append("!");
    Mockito.verify(spyBuilder).append("!");

    // toString() still returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
}

现在编译更新后的代码:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

代码应该能无错误地编译。在这一步中,我们了解了模拟对象 (Mock) 和间谍 (Spy) 的关键区别,以及何时使用它们。模拟对象 (Mock) 会完全替换原始对象的行为,而间谍 (Spy) 会保留原始行为,但在需要时允许你覆盖特定的方法。

总结

在本次实验中,我们探索了 Mockito 中间谍 (Spy) 的概念,这是 Java 单元测试中的一个强大特性。以下是我们涵盖的关键点:

  1. 创建间谍 (Spy):我们学习了如何使用 Mockito.spy() 方法和 @Spy 注解来创建间谍 (Spy)。前者更灵活,而后者在你需要创建多个间谍 (Spy) 时更方便。

  2. 间谍 (Spy) 的行为:间谍 (Spy) 像模拟对象 (Mock) 一样跟踪方法调用,但默认情况下会执行真实的方法。这种双重特性使它们在测试真实对象的同时,仍能验证交互,特别有用。

  3. 对间谍 (Spy) 进行存根 (Stubbing) 处理:我们了解了如何使用 doReturn()doThrow() 和其他存根 (Stubbing) 方法来覆盖间谍 (Spy) 方法的默认行为。这使我们能够在保持其他行为不变的情况下,控制特定的行为。

  4. 异常处理:我们探讨了在普通对象上使用 Mockito 验证方法时会出现的 NotAMockException,以及如何正确处理它。

  5. 模拟对象 (Mock) 与间谍 (Spy) 的比较:我们比较了模拟对象 (Mock) 和间谍 (Spy),以了解它们的关键区别。模拟对象 (Mock) 用默认或存根 (Stubbing) 的响应完全替换原始行为,而间谍 (Spy) 保留原始行为,但允许覆盖特定的方法。

间谍 (Spy) 是 Mockito 工具集中的强大工具,它在模拟对象 (Mock) 的完全控制和真实对象的真实性之间取得了平衡。在以下情况下,它们特别有用:

  • 你想测试一个真实对象,但需要验证某些交互。
  • 你只需要覆盖特定的方法,同时保持对象的其他行为不变。
  • 你处理的是复杂对象,对所有方法进行模拟 (Mock) 是不切实际的。

通过了解何时以及如何使用间谍 (Spy),你可以为 Java 应用程序编写更有效、更易于维护的单元测试。