JUnit 5和Mockito进行单元测试

JUnit 5 基础

JUnit 5是最新的JUnit版本,它引入了许多新特性,包括更灵活的测试实例生命周期、参数化测试、更丰富的断言和假设等。

  • 基本注解

    • @Test:标记一个方法为测试方法。
    • @BeforeEach:在每个测试方法之前执行。
    • @AfterEach:在每个测试方法之后执行。
    • @BeforeAll:在所有测试方法之前执行一次(必须是静态方法)。
    • @AfterAll:在所有测试方法之后执行一次(必须是静态方法)。
    • @DisplayName:定义测试类或测试方法的自定义名称。
    • @Nested:允许将测试类分组到更小的测试类中。
    • @ParameterizedTest:进行参数化测试。
  • 断言Assertions类):

    • assertEquals(expected, actual):验证两个对象或值是否相等。
    • assertTrue(condition):验证条件是否为真。
    • assertFalse(condition):验证条件是否为假。
    • assertThrows(exception, executable):验证执行特定代码块时是否抛出预期异常。
    • assertAll(executables):组合多个断言,确保所有断言都必须通过。

Mockito 基础

Mockito是一个流行的Java mocking框架,用于在隔离环境中测试代码,通过模拟依赖来确保测试的独立性。

  • 基本注解

    • @Mock:创建一个模拟对象。
    • @InjectMocks:创建一个实例,其字段或构造器依赖将被@Mock注解的模拟对象自动注入。
    • @Spy:可以创建一个真实的对象,并在需要时对它的某些方法进行模拟。
    • @Captor:用于捕获方法调用的参数。
  • 常用方法

    • when(mock.method()).thenReturn(value):指定当调用模拟对象的某个方法时应返回的值。
    • verify(mock).method():验证模拟对象的某个方法是否被调用。
    • any(), eq(), anyInt() 等:在when()verify()方法中使用的参数匹配器。

示例

假设我们有一个PaymentService类,它依赖于PaymentProcessor接口:

java 复制代码
public class PaymentService {
    private PaymentProcessor processor;

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

    public boolean process(double amount) {
        return processor.processPayment(amount);
    }
}

下面是如何使用JUnit 5和Mockito来测试PaymentService类:

java 复制代码
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
public class PaymentServiceTest {

    @Mock
    PaymentProcessor processor;

    @Test
    public void testProcessPayment() {
        // 设置
        PaymentService service = new PaymentService(processor);
        double amount = 100.0;
        when(processor.processPayment(amount)).thenReturn(true);

        // 执行
        boolean result = service.process(amount);

        // 验证
        assertTrue(result);
        verify(processor).processPayment(amount);
    }
}

在这个例子中,我们使用@Mock来创建PaymentProcessor的模拟对象,并使用when(...).thenReturn(...)来定义当调用processPayment方法时应返回的值。然后,我们执行process方法,并使用assertTrue来验证结果是否符合预期。最后,我们使用verify来确认processPayment方法是否被正确调用。

JUnit 5 进阶用法

参数化测试(Parameterized Tests)

参数化测试允许你使用不同的参数多次运行同一个测试。这对于需要验证多种输入条件的方法特别有用。

java 复制代码
@ParameterizedTest
@ValueSource(strings = {"Hello", "JUnit"})
void withValueSource(String word) {
    assertNotNull(word);
}
动态测试(Dynamic Tests)

JUnit 5允许你动态生成测试,这些测试可以在运行时根据代码逻辑来决定。

java 复制代码
@TestFactory
Collection<DynamicTest> dynamicTests() {
    return Arrays.asList(
        dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))),
        dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))
    );
}
嵌套测试(Nested Tests)

使用@Nested注解,你可以将相关的测试组织在一起作为一个组在外层测试类中运行。

java 复制代码
@Nested
class WhenNew {
    @Test
    void isEmpty() {
        assertEquals(0, new ArrayList<>().size());
    }
​
    @Nested
    class AfterAddingAnElement {
        @Test
        void isNotEmpty() {
            List<Object> list = new ArrayList<>();
            list.add(new Object());
​
            assertEquals(1, list.size());
        }
    }
}

Mockito 进阶用法

使用@Spy进行部分模拟

有时你可能需要模拟类的某些方法,而保持其他方法的实际行为。@Spy注解允许你这样做。

java 复制代码
@Spy
List<String> spyList = new ArrayList<>();
​
@Test
void testSpy() {
    spyList.add("one");
    spyList.add("two");
​
    verify(spyList).add("one");
    verify(spyList).add("two");
​
    assertEquals(2, spyList.size()); // 实际调用方法
​
    // 修改方法行为
    doReturn(100).when(spyList).size();
    assertEquals(100, spyList.size()); // 方法行为被改变
}
参数捕获(Argument Captors)

有时在验证方法调用时,你可能对方法调用的具体参数值感兴趣。@Captor注解和ArgumentCaptor类允许你捕获和检查这些值。

java 复制代码
@Mock
List<String> mockList;
​
@Captor
ArgumentCaptor<String> argCaptor;
​
@Test
void argumentCaptorTest() {
    mockList.add("one");
    verify(mockList).add(argCaptor.capture());
​
    assertEquals("one", argCaptor.getValue());
}
连续调用的不同返回值

有时候,你可能需要一个方法在连续调用时返回不同的值。Mockito允许你通过thenReturn()方法链来实现这一点。

java 复制代码
when(mockList.size()).thenReturn(0).thenReturn(1);
assertEquals(0, mockList.size());
assertEquals(1, mockList.size());
验证调用次数

验证一个方法被调用了特定次数。

java 复制代码
mockList.add("once");
mockList.add("twice");
mockList.add("twice");
​
verify(mockList).add("once");
verify(mockList, times(2)).add("twice");
verify(mockList, never()).add("never happened");

JUnit 5

超时测试

JUnit 5允许你为测试设置超时时间,确保测试在给定时间内完成。如果超出指定时间,测试将失败。

java 复制代码
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void timeoutTest() {
    // 模拟一个耗时的操作
    // 如果操作超过500毫秒,则测试失败
}
重复测试

如果你想对一个测试方法进行多次执行以确保其稳定性或寻找潜在的偶发问题,可以使用@RepeatedTest注解。

java 复制代码
@RepeatedTest(5)
void repeatTest() {
    // 这个测试会运行5次
}
条件执行

JUnit 5提供了多种条件执行测试的方法,这些方法可以基于不同的条件来决定是否执行某个测试,例如操作系统类型、环境变量或Java版本。

java 复制代码
@Test
@EnabledOnOs(OS.WINDOWS)
void onlyOnWindows() {
    // 仅在Windows操作系统上运行
}
​
@Test
@EnabledIfSystemProperty(named = "user.name", matches = "yourUserName")
void onlyForSpecificUser() {
    // 仅当系统用户名匹配时运行
}

Mockito

模拟静态方法(需要Mockito 3.4.0及以上版本)

从Mockito 3.4.0开始,你可以使用mockStatic来模拟静态方法。这是通过try-with-resources语句来实现的,以确保静态mock在使用后被正确关闭。

java 复制代码
try (MockedStatic<UtilityClass> mockedStatic = mockStatic(UtilityClass.class)) {
    mockedStatic.when(UtilityClass::someStaticMethod).thenReturn("mocked response");
    assertEquals("mocked response", UtilityClass.someStaticMethod());
    // 静态方法被模拟期间的行为
}
// 在这个块之外,静态方法恢复原有行为
模拟final方法和类

Mockito 2.x开始支持模拟final方法和类。为了启用这个功能,你需要在src/test/resources/mockito-extensions目录下创建一个名为org.mockito.plugins.MockMaker的文件,并在文件中添加一行内容:

复制代码
mock-maker-inline

这样配置后,Mockito就可以模拟final类和方法了。

使用BDDMockito进行行为驱动开发

BDDMockito提供了一种基于行为驱动开发(BDD)的语法来编写Mockito测试,使得测试更加可读。

java 复制代码
@Test
void bddStyleTest() {
    // 给定
    BDDMockito.given(mockList.size()).willReturn(2);
​
    // 当
    int size = mockList.size();
​
    // 那么
    BDDMockito.then(mockList).should().size();
    assertEquals(2, size);
}

@Mock 、@InjectMocks的原理

@Mock

  • 原理@Mock注解告诉Mockito框架为标注的字段生成一个模拟对象。这个模拟对象是动态生成的代理对象,它拦截对任何非final方法的调用,并允许测试者通过Mockito的API来配置这些调用的行为(例如返回特定的值或抛出异常)。
  • 如何工作 :当测试初始化时(例如,通过使用MockitoAnnotations.initMocks(this)方法或JUnit 5的@ExtendWith(MockitoExtension.class)),Mockito会扫描测试类中所有使用@Mock注解的字段,并为它们创建模拟对象。这些模拟对象默认不执行任何实际的代码逻辑,它们的行为完全由测试者通过Mockito的API来控制。

@InjectMocks

  • 原理@InjectMocks注解用于自动将@Mock(或@Spy)注解创建的模拟对象注入到被注解的字段中。Mockito会尝试通过构造器注入、属性注入或setter方法注入的方式,将模拟对象注入到@InjectMocks标注的实例中。
  • 如何工作
    1. 构造器注入:Mockito首先尝试使用包含最多参数的构造器来创建实例。如果构造器的参数能够与已声明的模拟对象匹配,这些模拟对象将被用作构造器参数。
    2. 属性注入:如果构造器注入不适用或不成功,Mockito会尝试直接设置实例中与模拟对象类型相匹配的属性。
    3. Setter注入:最后,如果属性注入不成功,Mockito会尝试通过调用匹配的setter方法来注入模拟对象。
相关推荐
niuniu_6662 天前
Selenium 性能测试指南
selenium·测试工具·单元测试·测试·安全性测试
互联网杂货铺2 天前
黑盒测试、白盒测试、集成测试和系统测试的区别与联系
自动化测试·软件测试·python·功能测试·测试工具·单元测试·集成测试
佟格湾3 天前
几种常见的.NET单元测试模拟框架介绍
单元测试
niuniu_6663 天前
安全性测试(Security Testing)
测试工具·单元测试·appium·测试·安全性测试
niuniu_6663 天前
selenium应用测试场景
python·selenium·测试工具·单元测试·测试
噔噔噔噔@3 天前
软件测试对于整个行业的重要性及必要性
python·单元测试·压力测试
俞凡4 天前
如何编写更好的单元测试
单元测试·测试
WIN赢4 天前
单元测试的编写
单元测试·log4j
测试老哥4 天前
什么是集成测试?集成的方法有哪些?
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·集成测试
杨凯凡5 天前
Mockito 全面指南:从单元测试基础到高级模拟技术
java·单元测试·mockito