参数化测试与 Mock
约 849 字大约 3 分钟
布欧-Lewyon
2026-05-15
当测试逻辑相同、仅输入数据不同时,参数化测试可避免重复编写多个 @Test 方法。当被测代码依赖外部服务时,Mock 替身让测试聚焦于自身逻辑。
参数化测试
JUnit 5 通过 @ParameterizedTest + 数据源注解实现。需添加依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>@ValueSource —— 简单值
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "level", "madam"})
void testPalindrome(String word) {
assertTrue(StringUtils.isPalindrome(word));
}@CsvSource —— 多参数
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, -5, 5"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}@CsvFileSource —— 从 CSV 文件读取
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
void testFromFile(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}src/test/resources/test-data.csv:
a,b,expected
1,1,2
2,3,5@MethodSource —— 由方法提供数据
@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String input) {
assertNotNull(input);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana", "cherry");
}数据源方法必须是 static,返回 Stream、Collection、Iterator 或数组。
指定测试名称
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({ "1, 1, 2", "2, 3, 5" })
void testNamed(int a, int b, int expected) {
// 测试报告中显示 "1 + 1 = 2"
}Mockito 入门
Mockito 是 Java 使用最广泛的 Mock 框架。它创建接口或类的假实现(Mock),让你可以指定返回值、验证调用行为。
依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>创建 Mock 与 Stub
被测类 UserService 依赖 UserRepository:
public interface UserRepository {
User findById(String id);
void save(User user);
}
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo;
}
public String getUserName(String id) {
User user = repo.findById(id);
return user != null ? user.getName() : "UNKNOWN";
}
}测试类:
import static org.mockito.Mockito.*;
class UserServiceTest {
@Test
void testGetUserName() {
// 1. 创建 Mock
UserRepository mockRepo = mock(UserRepository.class);
// 2. Stub:指定 findById 的行为
when(mockRepo.findById("1")).thenReturn(new User("1", "Alice"));
when(mockRepo.findById("2")).thenReturn(null);
// 3. 注入 Mock
UserService service = new UserService(mockRepo);
// 4. 验证
assertEquals("Alice", service.getUserName("1"));
assertEquals("UNKNOWN", service.getUserName("2"));
}
}验证交互
除了 Stub 返回值,还可验证方法是否被调用、调用次数和顺序:
@Test
void testSaveUser() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
User user = new User("3", "Bob");
service.updateUserName("3", "Bobby");
// 验证 save 被调用了一次,且参数正确
verify(mockRepo, times(1)).save(argThat(u ->
u.getId().equals("3") && u.getName().equals("Bobby")));
}常用验证模式:
verify(mock, times(1)).method(); // 刚好 1 次(默认)
verify(mock, atLeast(1)).method(); // 至少 1 次
verify(mock, atMost(3)).method(); // 至多 3 次
verify(mock, never()).method(); // 从未调用
verifyNoInteractions(mock); // 无任何交互@Mock / @InjectMocks 注解
使用注解简化 Mock 创建与依赖注入:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository mockRepo;
@InjectMocks
private UserService service; // 自动注入 mockRepo
@Test
void testGetUserName() {
when(mockRepo.findById("1")).thenReturn(new User("1", "Alice"));
assertEquals("Alice", service.getUserName("1"));
}
}小结
@ParameterizedTest+@ValueSource/@CsvSource/@MethodSource等实现数据驱动测试,减少重复代码。- Mockito 通过
mock()/@Mock创建假对象,when().thenReturn()指定行为。 verify()验证方法调用次数和参数,确保被测代码与依赖的交互符合预期。@InjectMocks自动将@Mock注入到被测对象中。- 易错:Mock 的
when().thenReturn()只在参数精确匹配时生效;使用any()、eq()等参数匹配器时,所有参数都必须用匹配器包裹,不能混用。 - 思考任务:给上面
UserService的updateUserName方法写完整测试:Mock UserRepository,验证findById和save的正确调用,并测试 ID 不存在时的异常行为。
上一节:JUnit 5 基础
下一节:SLF4J 与 Logback
