小王学习录
- 依赖
- 注解
- Mock
- @Spy
- 静态方法单元测试
- @InjectMocks 注解
- @Captor 注解
- @BeforeAll 和 BeforeEach的区别
- @ParameterizedTest
- @ValueSource
- @EnumSource
- @CsvSource
- @MethodSource
- 打桩
- 打桩方式
- 打桩参数匹配方式
依赖
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline --><dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>5.2.0</version><scope>test</scope></dependency><!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.10.2</version><scope>test</scope></dependency>
注解
Mock
class MockTestTest {@Mockprivate MockTest demo;@BeforeEachvoid setup(){MockitoAnnotations.openMocks(this);}@Testvoid add() {Mockito.when(demo.add(1, 2)).thenReturn(3);System.out.println(demo.add(1, 2));Assertions.assertEquals(3, demo.add(1, 2));Mockito.verify(demo, Mockito.times(2)).add(1, 2);}@AfterEachvoid after(){System.out.println("测试完毕 ");}
}
@Spy
class SpyTestTest {@Spyprivate SpyTest spyTest;@BeforeEachvoid setUp(){MockitoAnnotations.openMocks(this);}@Testvoid add() {Mockito.when(spyTest.add(1, 2)).thenReturn(4);System.out.println(spyTest.add(1, 2));Mockito.verify(spyTest).add(1,2);Assertions.assertEquals(4, spyTest.add(1, 2));//Mockito.verify(spyTest, Mockito.times(2)).add(1, 2);Mockito.when(spyTest.add(1, 2)).thenCallRealMethod();System.out.println(spyTest.add(1, 2));}
}
静态方法单元测试
class StaticTestTest {@Testvoid add() {MockedStatic<StaticTest> test1 = Mockito.mockStatic(StaticTest.class);test1.when(()->StaticTest.add(1, 2)).thenReturn(4);System.out.println(StaticTest.add(1, 2));Assertions.assertEquals(4, StaticTest.add(1, 2));}
}
@InjectMocks 注解
@Captor 注解
@Captor 是 Mockito 框架中的一个注解,用于创建 ArgumentCaptor 实例。ArgumentCaptor 在单元测试中扮演着重要角色,它允许你在测试过程中捕获(capture)被测方法调用时传入的实际参数值,以便对这些参数进行详细的断言验证。
假设有一个 EmailService 类,它有一个方法 sendEmail(Recipient recipient, EmailMessage message),我们想要测试这个方法是否正确地将邮件发送给了预期的收件人。可以编写如下使用 @Captor 注解的单元测试:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;import static org.mockito.Mockito.verify;public class EmailServiceTest {@Mockprivate EmailGateway emailGateway; // Mocked dependency for sending emailsprivate EmailService emailService;@Captorprivate ArgumentCaptor<Recipient> recipientCaptor;@Captorprivate ArgumentCaptor<EmailMessage> messageCaptor;@BeforeEachvoid setUp() {MockitoAnnotations.initMocks(this);emailService = new EmailService(emailGateway);}@Testpublic void testSendEmail() {Recipient expectedRecipient = new Recipient("john.doe@example.com");EmailMessage expectedMessage = new EmailMessage("Subject", "Body");emailService.sendEmail(expectedRecipient, expectedMessage);// 使用 captors 捕获实际传递给 sendEmail 方法的参数verify(emailGateway).sendEmail(recipientCaptor.capture(), messageCaptor.capture());// 使用 captured 参数进行断言Recipient actualRecipient = recipientCaptor.getValue();EmailMessage actualMessage = messageCaptor.getValue();assertEquals(expectedRecipient.getEmail(), actualRecipient.getEmail());assertEquals(expectedMessage.getSubject(), actualMessage.getSubject());assertEquals(expectedMessage.getBody(), actualMessage.getBody());}
}
- 使用 @Mock 注解创建了 EmailGateway 的 mock 对象。
- 使用 @Captor 注解声明了两个 ArgumentCaptor 实例,分别对应 sendEmail 方法的 Recipient 和 EmailMessage 参数。
- 在 setUp 方法中初始化 mock 和被测试的 EmailService。
- 在 testSendEmail 测试方法中,调用 emailService.sendEmail() 方法,传入期望的参数。
- 使用 verify 方法验证 emailGateway.sendEmail() 是否被调用,并且使用 captors 捕获实际传递的参数。
- 最后,通过 captors 获取捕获的参数值,并进行详细的断言验证,确保它们与预期值相匹配。
- 通过 @Captor 注解和 ArgumentCaptor,我们能够有效地验证 sendEmail 方法是否正确地将预期的收件人和邮件内容传递给了依赖的 EmailGateway。
@BeforeAll 和 BeforeEach的区别
@BeforeAll 和 @BeforeEach 是 JUnit 5 中用于在测试执行前进行初始化工作的两个注解,它们的主要区别在于执行时机和适用场景:
@BeforeAll
执行时机:
@BeforeAll 注解的方法将在当前测试类中所有测试方法执行之前只执行一次。
适用场景:
- 一次性昂贵资源的初始化:当测试类中的所有测试方法都需要共享一个昂贵的资源(如数据库连接、网络连接、第三方服务客户端等),并且创建或连接这个资源的成本较高时,使用 @BeforeAll 来初始化一次,避免在每个测试方法前重复执行。
- 全局数据加载:如果有一组测试数据需要从文件、数据库或其他外部源加载,且所有测试方法都会使用同一份数据,那么可以使用 @BeforeAll 来提前加载数据,避免多次加载带来的性能开销。
- 长时间运行的预设条件:当有一些耗时较长但对所有测试方法通用的预设条件(如模拟复杂环境、配置全局系统状态等),应放在 @BeforeAll 注解的方法中执行。
- 静态环境配置:如果需要对静态变量、系统属性或全局配置进行一次性设定,适用于 @BeforeAll。
注意事项:
@BeforeAll 注解的方法必须是静态方法
,因为它们在测试类实例化之前执行。
如果这些方法抛出异常,所有测试方法都将跳过执行。
@BeforeEach
执行时机:
@BeforeEach 注解的方法将在每个测试方法执行之前单独执行一次。
适用场景:
测试数据的独立准备:每个测试方法可能需要不同的测试数据或对象状态,使用 @BeforeEach 可以为每个测试方法单独准备所需的测试数据或对象实例。
对象重置:如果被测试对象具有状态,且每次测试方法执行后需要将其状态重置为初始状态,@BeforeEach 方法可以负责这项清理和重置工作。
局部依赖注入:对于那些仅在一个测试方法执行期间有效的依赖项,可以在 @BeforeEach 方法中注入。
重复性环境配置:对于每次测试开始时都需要重新配置的环境设置(如清除缓存、重置模拟对象状态等),应在 @BeforeEach 注解的方法中进行。
注意事项:
@BeforeEach 注解的方法不能是静态的,因为它们与每个测试方法的执行紧密相关,通常需要访问非静态的成员变量或方法。
如果某个 @BeforeEach 方法执行失败(抛出异常),仅影响紧随其后的那个测试方法,其他测试方法仍会尝试执行各自的 @BeforeEach 方法。
总结来说,@BeforeAll 用于执行一次即可满足整个测试类需求的、成本较高的初始化操作,适用于共享资源的设置和全局数据的加载;而 @BeforeEach 则是在每个测试方法执行前进行针对性的、可能因测试方法不同而有所差异的准备工作,确保每个测试方法在独立、一致的环境中运行。选择使用哪一个注解取决于测试场景的具体需求和资源管理策略。
@ParameterizedTest
@ParameterizedTest 是 JUnit 5 提供的一种用于执行参数化测试的注解。参数化测试允许同一个测试方法使用不同的输入数据集(参数)多次执行,从而提高测试覆盖率和代码复用性。通过这种方式,可以轻松地验证被测代码在多种不同情况下的行为,确保其正确性和一致性。
通常需要配合@ValueSource、@EnumSource、@CsvSource、@MethodSource一起使用。
@ValueSource
用途:@ValueSource 用于为参数化测试提供一组固定的值。这些值可以是基本类型(如 int、double、String 等)或其对应的包装类型。
使用场景:
测试某个方法或函数在一系列特定值上的表现,如验证数学函数在不同整数、浮点数上的计算结果。
验证类的构造函数或方法对各种边界值(如最大值、最小值、零值、正负值等)的处理是否正确。
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testSquareRoot(int input, int expectedOutput) {double result = Math.sqrt(input);assertEquals(expectedOutput, result, 0.01, "Square root of " + input + " should be close to " + expectedOutput);
}
@EnumSource
用途:@EnumSource 专门用于为参数化测试提供枚举类型的值。它可以指定要使用的枚举类,并可以选择性地提供筛选条件(如包含特定名称或值的枚举常量)。
使用场景:
对枚举类型的类进行全面测试,确保其每个枚举常量在特定场景下的行为正确。
当测试方法需要验证针对枚举类型的逻辑时,如序列化、反序列化、字符串转换等。
enum Color { RED, GREEN, BLUE }@ParameterizedTest
@EnumSource(Color.class)
void testColorToString(Color color, String expectedString) {String result = color.toString();assertEquals(expectedString, result, "toString() should return the correct string for " + color);
}
@CsvSource
用途:@CsvSource 用于提供以逗号分隔值(CSV)格式的数据集。每一行代表一组参数,各参数间以逗号分隔。这对于多参数的测试方法非常方便,可以直接在注解中列出多行数据。
使用场景:
当测试需要多组不同参数组合时,如验证数学运算、字符串处理函数、日期格式转换等。
当测试数据以表格形式存储或展示时,可以直接复制粘贴成 CSV 格式。
@ParameterizedTest
@CsvSource({"1, 2, 3","10, 5, 15","-1, .jpg, .png","abc, def, abcdef"
})
void testConcatenate(String part1, String part2, String expectedConcatenation) {String result = concatenate(part1, part2);assertEquals(expectedConcatenation, result, "Concatenation of " + part1 + " and " + part2 + " should be " + expectedConcatenation);
}
@MethodSource
用途:@MethodSource 允许参数化测试的数据来源于测试类中的一个静态方法。这个方法返回一个 Stream,其中每个 Arguments 对象封装了一组参数。这种方法提供了最大的灵活性,可以生成动态数据、读取外部资源、甚至执行复杂的逻辑来提供测试参数。
使用场景:
数据集较大或需要动态生成时,可以编写一个方法来按需生成或读取数据。
测试需要依赖外部配置文件、数据库查询结果、随机数据生成器等复杂数据源。
需要根据测试环境动态调整测试参数。
static Stream<Arguments> additionTestData() {return Stream.of(Arguments.of(1, 2, 3),Arguments.of(-1, 3, 2),Arguments.of(0, 0, 0),Arguments.of(100, -50, 50),Arguments.of(1.5, 2.75, 4.25));
}@ParameterizedTest
@MethodSource("additionTestData")
void testAdd(double num1, double num2, double expectedSum) {double result = calculator.add(num1, num2);assertEquals(expectedSum, result, "The sum should be correct for inputs: " + num1 + ", " + num2);
}
总结起来,@ValueSource、@EnumSource、@CsvSource 和 @MethodSource 分别提供了不同类型和来源的参数数据集
打桩
打桩方式
1、when().thenReturn()
2、doReturn().when()
以上两种是有返回值的方法打桩,通常使用。
3、doNothing().when()…
void方法打桩;
4、when().thenThrow()
5、doThrow().when()
以上两种是抛异常打桩;
6、when().thenAnswer()
7、doAnswer().when()
以上两种是复杂参数打桩,可以根据传参自定义返回结果。
8、@InjectMocks+@Spy,打桩被测类的某个方法
正常被单测对象使用@InjectMocks注解,被测业务逻辑复杂,比如被测方法A()→B()→C()。如果当前单元测试用例不打算测试到C()方法,就需要结合@Spy和具体打桩方式跳过C()方法。
打桩参数匹配方式
打桩时,如果被测方法有输入参数,有以下方式传参:
any()、anyList()、anyByte()、anyBoolean()、anyChar()、anyCollection()、anyDouble()、anyFloat()、anyInt()、anyIterable()、anyLong()、anyMap()、anySet()、anyShort()、anyString()
通常仅需要any(),如果被打桩的测试方法有重载,则需要指定具体的参数类型。
这些传参方法是Mockito框架中用于创建“任意匹配器”的工具,它们在编写单元测试时非常有用,特别是当我们要验证方法被调用时的参数类型和数量,而不关心具体的参数值时。简单举几个例子
- any():
用途:匹配任何类型的对象。当被测方法接受的对象类型不确定或不关心其具体值时使用。
import static org.mockito.Mockito.*;class Service {void process(Object input) {// ...}
}@Test
void testProcessMethod() {Service service = mock(Service.class);service.process(any()); // 匹配任何类型的对象作为输入参数verify(service).process(any()); // 验证process方法被调用时,传入了任意对象
}
- anyList():
用途:匹配任何类型的列表(实现 List 接口的对象)。当被测方法接受一个列表作为参数,但不关心列表的具体元素时使用。
import static org.mockito.Mockito.*;class Service {void handleList(List<String> items) {// ...}
}@Test
void testHandleListMethod() {Service service = mock(Service.class);service.handleList(anyList()); // 匹配任何类型的列表作为输入参数verify(service).handleList(anyList()); // 验证handleList方法被调用时,传入了任意列表
}
- anyCollection():
用途:匹配任何类型的 Collection 对象(实现了 Collection 接口的对象)。当被测方法接受一个集合作为参数,但不关心集合的具体元素时使用。
import static org.mockito.Mockito.*;class Service {void handleCollection(Collection<String> items) {// ...}
}@Test
void testHandleCollectionMethod() {Service service = mock(Service.class);service.handleCollection(anyCollection()); // 匹配任何类型的 Collection 对象作为输入参数verify(service).handleCollection(anyCollection()); // 验证handleCollection方法被调用时,传入了任意 Collection 对象
}
- anyIterable():
用途:匹配任何实现了 Iterable 接口的对象。当被测方法接受一个可迭代对象作为参数,但不关心其具体元素时使用。
import static org.mockito.Mockito.*;class Service {void processIterable(Iterable<String> items) {// ...}
}@Test
void testProcessIterableMethod() {Service service = mock(Service.class);service.processIterable(anyIterable()); // 匹配任何 Iterable 对象作为输入参数verify(service).processIterable(anyIterable()); // 验证processIterable方法被调用时,传入了任意 Iterable 对象
}
- anyMap():
用途:匹配任何类型的 Map 对象(实现了 Map 接口的对象)。当被测方法接受一个映射作为参数,但不关心其具体键值对时使用。
import static org.mockito.Mockito.*;class Service {void updateData(Map<String, Integer> data) {// ...}
}@Test
void testUpdateDataMethod() {Service service = mock(Service.class);service.updateData(anyMap()); // 匹配任何 Map 对象作为输入参数verify(service).updateData(anyMap()); // 验证updateData方法被调用时,传入了任意 Map 对象
}