Mock 框架文档(Mockito / PowerMock / WireMock)
Mockito 核心对象创建
Mockito 是 Java 生态中最流行的 Mock 框架,提供多种方式创建 Mock 对象。
mock() 静态方法
通过 Mockito.mock() 直接创建 Mock 对象,适用于不需要依赖注入的场景。
import static org.mockito.Mockito.*;
// 创建 Mock 对象
List<String> mockedList = mock(List.class);
UserService userService = mock(UserService.class);
// 使用 Mock 对象
mockedList.add("one");
when(mockedList.size()).thenReturn(100);@Mock 注解
使用注解方式声明 Mock 对象,需要在测试类中显式调用 MockitoAnnotations.openMocks() 或在测试类上添加 @ExtendWith(MockitoExtension.class)(JUnit 5)。
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
// 测试方法...
}对于 JUnit 4,使用 @RunWith(MockitoJUnitRunner.class)。
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
// 测试方法...
}@InjectMocks 注解
@InjectMocks 自动将标记了 @Mock 或 @Spy 的依赖注入到被测试对象中。Mockito 会尝试通过构造函数注入、setter 注入或字段注入来完成注入。
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentGateway paymentGateway;
@Mock
private NotificationService notificationService;
@InjectMocks // Mockito 自动将上面的 Mock 注入到 OrderService 中
private OrderService orderService;
@Test
void testCreateOrder() {
// 使用已注入 Mock 的 orderService 进行测试
}
}注入优先级:构造函数注入 > Setter 注入 > 字段注入。
@Spy 注解
@Spy 创建真实对象的部分 Mock(Spy),默认调用真实方法,但可以通过 when().thenReturn() 或 doReturn() 选择性覆盖。
@ExtendWith(MockitoExtension.class)
class AuditServiceTest {
@Spy
private AuditLogger auditLogger = new AuditLogger(); // 需要提供实例
@InjectMocks
private AuditService auditService;
@Test
void testPartialMock() {
// 仅 Mock 某个方法,其余调用真实逻辑
doNothing().when(auditLogger).flush();
auditService.record("test");
}
}spy() 静态方法同样支持:
AuditLogger spyLogger = spy(new AuditLogger());@Captor 注解
@Captor 简化 ArgumentCaptor 的创建,用于捕获方法调用时传入的参数,便于后续断言。
@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {
@Mock
private MessageQueue messageQueue;
@InjectMocks
private NotificationService notificationService;
@Captor
private ArgumentCaptor<Notification> notificationCaptor;
@Test
void testSendNotification() {
notificationService.send("user@example.com", "Hello");
verify(messageQueue).publish(notificationCaptor.capture());
Notification captured = notificationCaptor.getValue();
assertEquals("user@example.com", captured.getRecipient());
assertEquals("Hello", captured.getContent());
}
}Mockito 行为定义
when().thenReturn() — 方法调用返回指定值
最常用的行为定义方式,适用于非 void 方法。
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));
// 链式返回:第一次调用返回 user1,第二次返回 user2
when(userRepository.findById(anyLong()))
.thenReturn(Optional.of(user1))
.thenReturn(Optional.of(user2));
// 等效写法,使用参数
when(userRepository.findById(anyLong()))
.thenReturn(Optional.of(user1), Optional.of(user2));doThrow() — 方法调用抛出异常
适用于 void 方法或需要抛出异常的场景。
// void 方法抛异常
doThrow(new RuntimeException("DB connection failed"))
.when(userRepository).delete(any());
// 非 void 方法同样可以
doThrow(new DataAccessException("Timeout"))
.when(userRepository).findById(999L);
// 条件式:第一次正常,第二次抛异常
doNothing()
.doThrow(new RuntimeException("Rate limit exceeded"))
.when(rateLimiter).checkLimit(anyString());doAnswer() — 自定义方法行为
当需要根据输入参数动态决定返回值时使用。
doAnswer(invocation -> {
Long id = invocation.getArgument(0);
User user = new User();
user.setId(id);
user.setName("User_" + id);
return Optional.of(user);
}).when(userRepository).findById(anyLong());
// 更简洁的 Lambda 写法
doAnswer(inv -> {
User u = inv.getArgument(0);
u.setId(System.currentTimeMillis());
return null; // void 方法返回 null
}).when(userRepository).save(any(User.class));Answer 接口的核心方法是 answer(InvocationOnMock invocation),通过 invocation.getArgument(index) 获取参数,通过 invocation.getMethod() 获取方法信息。
doNothing() — void 方法什么也不做
对于 void 方法,Mockito 默认就是什么都不做(空的 Mock 实现通常也什么也不做),但 doNothing() 在以下场景有用:
// 显式声明 void 方法无操作
doNothing().when(notificationService).sendEmail(anyString());
// 链式:第一次发送,第二次什么都不做
doNothing()
.doThrow(new MailServerUnavailableException())
.when(notificationService).sendEmail(anyString());doCallRealMethod() — 调用真实方法
用于 Spy 对象或 Mock 对象上需要部分调用真实逻辑的场景。
// Mock 对象上调用真实方法
UserService userService = mock(UserService.class);
doCallRealMethod().when(userService).getUserName(anyLong());
// 此时 userService.getUserName(1L) 会执行真实逻辑
// 但 userService 的其他方法仍然是 Mock 行为
// Spy 对象上也可以使用
AuditLogger spyLogger = spy(new AuditLogger());
doCallRealMethod().when(spyLogger).writeLog(anyString());void 方法的 Mock 全览
void 方法的 Mock 方式汇总:
| Mock 方式 | 适用场景 | 示例 |
|---|---|---|
doNothing() | 让 void 方法什么都不做 | doNothing().when(obj).method() |
doThrow() | void 方法抛出异常 | doThrow(RuntimeException.class).when(obj).method() |
doAnswer() | 自定义 void 方法行为 | doAnswer(inv -> { ... return null; }).when(obj).method() |
doCallRealMethod() | 调用真实实现 | doCallRealMethod().when(spy).method() |
注意:对于 void 方法不能使用 when().thenReturn() 语法,必须使用 doXxx().when() 形式。
Mockito 验证
verify() — 验证方法是否被调用
// 验证方法被调用过(默认 times(1))
verify(userRepository).findById(1L);
verify(userRepository, times(1)).findById(1L); // 等效
// 验证从未被调用
verify(userRepository, never()).delete(any());
// 验证被调用 N 次
verify(userRepository, times(3)).save(any(User.class));
// 验证至少/至多
verify(userRepository, atLeastOnce()).findById(anyLong());
verify(userRepository, atLeast(2)).findAll();
verify(userRepository, atMost(3)).delete(any());
// 验证特定次数范围
verify(userRepository, timeout(100).times(2)).save(any());verifyNoMoreInteractions() / verifyZeroInteractions()
// 验证 Mock 对象上除了已验证的调用外,没有其他多余的交互
verify(userRepository).findById(1L);
verify(userRepository).save(user);
verifyNoMoreInteractions(userRepository); // 如果还有其他未验证的调用则失败
// 验证 Mock 对象完全没有发生过任何交互
verifyZeroInteractions(emailService); // deprecated 但常用
verifyNoInteractions(emailService); // 推荐的替代方法ArgumentMatchers — 参数匹配器
import static org.mockito.ArgumentMatchers.*;
// 任意参数
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
verify(userRepository).save(any(User.class));
// 任意字符串
when(userService.findByName(anyString())).thenReturn(user);
when(userService.findByName(any(String.class))).thenReturn(user);
// 任意 int / 特定范围的 int
when(userService.findByAge(anyInt())).thenReturn(users);
when(userService.findByAge(geq(18))).thenReturn(adultUsers); // >= 18
// 模式匹配
when(userService.findByEmail(matches(".*@example\\.com")))
.thenReturn(user);
// 自定义匹配器
when(userRepository.findById(argThat(id -> id > 0 && id < 1000)))
.thenReturn(Optional.of(user));
// 混合使用:固定参数 + 匹配器(所有参数都必须用匹配器或全部不用)
// 错误:when(userService.updateUser(1L, any(User.class))) // 编译错误
// 正确:when(userService.updateUser(eq(1L), any(User.class)))常见内建匹配器:
| 匹配器 | 说明 |
|---|---|
any() / any(Class) | 任意对象 |
anyInt() / anyLong() / anyString() | 任意基础类型 |
anyList() / anySet() / anyMap() | 任意集合 |
eq(value) | 等值匹配 |
same(value) | 同一对象引用 |
isNull() / isNotNull() | null 判断 |
contains(substring) | 包含子串 |
startsWith(prefix) | 前缀匹配 |
endsWith(suffix) | 后缀匹配 |
matches(regex) | 正则匹配 |
argThat(matcher) | 自定义匹配 |
ArgumentCaptor — 参数捕获
用于捕获调用参数并进行详细断言,比匹配器更灵活。
// 创建捕获器
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
// 或使用 @Captor 注解
// 执行
userService.deleteUser(1L);
// 捕获验证
verify(userRepository).delete(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals(1L, capturedUser.getId());
// 多次调用捕获
verify(userRepository, times(2)).save(userCaptor.capture());
List<User> allSavedUsers = userCaptor.getAllValues();
assertEquals(2, allSavedUsers.size());调用次数验证
| 验证模式 | 说明 |
|---|---|
times(n) | 精确调用 n 次 |
never() | 从未调用(等效于 times(0)) |
atLeastOnce() | 至少调用 1 次 |
atLeast(n) | 至少调用 n 次 |
atMost(n) | 至多调用 n 次 |
only() | 仅调用了该方法,无其他交互 |
timeout(millis) | 超时时间内完成指定次数 |
verify(orderRepository, times(1)).save(any());
verify(cacheService, never()).evict(any());
verify(eventBus, atLeastOnce()).publish(any());
verify(logger, atMost(5)).warn(anyString());
verify(metricService, only()).increment(any());
verify(messageQueue, timeout(5000).times(1)).send(any());Mockito 高级
@MockitoSettings / lenient()
Mockito 默认在遇到不必要的 Stubbing(未使用到的 Stubbing)时会发出警告。JUnit 4 下默认未通过测试,JUnit 5 下默认仅打日志。
// 在 JUnit 4 中使用 Lenient 模式
@RunWith(MockitoJUnitRunner.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class LenientTest {
// 宽松模式,容忍未使用的 Stubbing
}
// 对单个 Stubbing 使用 lenient()
@Test
void testLenientStub() {
lenient().when(userRepository.findById(999L)).thenReturn(Optional.empty());
// 即使 findById(999L) 在测试中从来不会被调用,也不会报错
}Strictness 枚举值:
| 级别 | 说明 |
|---|---|
STRICT_STUBS | 默认,未使用的 Stubbing 会报错 |
LENIENT | 容忍未使用的 Stubbing |
WARN | 仅打印警告,不中断测试 |
ReturningAnswer / 自定义 Answer
// 预定义的 ReturnsElementsOf
when(mockedList.get(anyInt()))
.thenAnswer(new ReturnsElementsOf(Arrays.asList("A", "B", "C")));
// 自定义 Answer 实现
Answer<User> dynamicUserAnswer = invocation -> {
Object[] args = invocation.getArguments();
Long id = (Long) args[0];
User user = new User();
user.setId(id);
user.setName("Auto_" + id);
return user;
};
when(userRepository.findById(anyLong()))
.thenAnswer(dynamicUserAnswer);
// 使用 Lambda 更简洁
when(userRepository.findById(anyLong()))
.thenAnswer(inv -> {
Long id = inv.getArgument(0);
return id <= 0 ? Optional.empty() : Optional.of(new User(id, "User_" + id));
});常用内建 Answer 实现:
| Answer 类 | 行为 |
|---|---|
ReturnsEmptyValues | 返回空值(默认) |
ReturnsElementsOf | 依次返回集合中的元素 |
ReturnsSmartNulls | 返回智能 null(不会 NPE) |
ReturnsMocks | 返回 Mock 对象 |
ReturnsDeepStubs | 深度 Stub,自动 Mock 链式调用 |
ThrowsExceptionClass | 抛出指定类型的异常 |
深度 Stub 示例:
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Customer customer;
@Test
void testDeepStub() {
// 无需手动 Mock getAddress().getCountry().getName()
when(customer.getAddress().getCountry().getName()).thenReturn("China");
assertEquals("China", customer.getAddress().getCountry().getName());
}BDDMockito
BDD(Behavior-Driven Development)风格的 Mockito API,语义更清晰。
import static org.mockito.BDDMockito.*;
@Test
void testBddStyle() {
// Given — 给定(Stubbing)
given(userRepository.findById(1L)).willReturn(Optional.of(user));
given(rateLimiter.tryAcquire()).willReturn(true);
// When — 触发(执行被测方法)
User result = userService.getUserById(1L);
// Then — 验证(断言 + 验证)
then(result).isNotNull();
then(result.getName()).isEqualTo("Alice");
then(userRepository).should(times(1)).findById(1L);
then(userRepository).shouldHaveNoMoreInteractions();
}BDDMockito 与标准 Mockito 的对应关系:
| 标准 Mockito | BDDMockito |
|---|---|
when(mock).thenReturn(v) | given(mock).willReturn(v) |
when(mock).thenThrow(e) | given(mock).willThrow(e) |
when(mock).thenAnswer(a) | given(mock).willAnswer(a) |
doThrow().when(mock) | willThrow().given(mock) |
verify(mock).call() | then(mock).should().call() |
verify(mock, times(n)) | then(mock).should(times(n)) |
verifyNoMoreInteractions(mock) | then(mock).shouldHaveNoMoreInteractions() |
PowerMock
PowerMock 通过自定义类加载器和字节码操作扩展了 Mockito,能够 Mock 那些常规 Mockito 无法处理的对象。
基础配置
PowerMock 需要与 Mockito 配合使用,并指定需要修改字节码的类。
// JUnit 4 配置
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(MockitoJUnitRunner.class) // 委托给 Mockito
@PrepareForTest({UserUtils.class, UserService.class}) // 需要修改字节码的类
public class PowerMockTest {
@Test
public void testStaticMock() {
// ...
}
}
// 如果使用 MockitoExtension,需要用 PowerMock 的扩展:
@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticClass.class})
public class AnotherTest {
// ...
}mockStatic — Mock 静态方法
这是 PowerMock 最典型的使用场景。
import static org.powermock.api.mockito.PowerMockito.*;
@RunWith(PowerMockRunner.class)
@PrepareForTest({IdGenerator.class, SecurityUtils.class})
public class StaticMockTest {
@Test
public void testMockStatic() {
// 启用静态 Mock
mockStatic(IdGenerator.class);
// 定义行为
when(IdGenerator.nextId()).thenReturn(1001L);
when(IdGenerator.formatId(anyLong()))
.thenReturn("ID-1001");
// 执行测试
Order order = orderService.createOrder(item);
// 验证静态方法调用
verifyStatic(IdGenerator.class, times(1));
IdGenerator.nextId();
}
}mockConstruction — Mock 构造方法
Mock 在代码中通过 new 创建的对象。
@RunWith(PowerMockRunner.class)
@PrepareForTest({OrderService.class}) // 包含 new 表达式的类
public class ConstructionMockTest {
@Test
public void testMockConstruction() {
// 当 OrderService 中 new PaymentGateway() 时,返回 Mock 对象
PaymentGateway mockGateway = mock(PaymentGateway.class);
when(mockGateway.charge(anyDouble())).thenReturn(true);
// 拦截构造方法调用
whenNew(PaymentGateway.class).withNoArguments()
.thenReturn(mockGateway);
// 或带参数的构造
whenNew(PaymentGateway.class)
.withArguments(anyString(), anyString())
.thenReturn(mockGateway);
// 执行测试 — OrderService 内部的 new PaymentGateway() 会被替换
Order order = orderService.createOrder(item);
// 验证构造方法被调用
verifyNew(PaymentGateway.class).withNoArguments();
}
}mockPrivate — Mock 私有方法
PowerMock 可以 Mock 类内部的私有方法。
@RunWith(PowerMockRunner.class)
@PrepareForTest({OrderService.class}) // 必须包含目标类
public class PrivateMethodMockTest {
@Test
public void testMockPrivate() throws Exception {
OrderService spyService = spy(new OrderService());
// Mock 私有方法
when(spyService, "calculateDiscount", anyDouble())
.thenReturn(0.0);
// 或使用 doReturn 方式
doReturn(0.0).when(spyService, "calculateDiscount", anyDouble());
// 执行公开方法,该公开方法内部调用私有方法
double result = spyService.getFinalPrice(100.0);
assertEquals(100.0, result, 0.01);
// 验证私有方法
verifyPrivate(spyService).invoke("calculateDiscount", 100.0);
}
}Whitebox — 反射工具
Whitebox 提供了便捷的反射操作,用于访问和修改私有字段/方法。
@RunWith(PowerMockRunner.class)
public class WhiteboxTest {
@Test
public void testWhitebox() {
OrderService service = new OrderService();
// 设置私有字段
Whitebox.setInternalState(service, "maxRetryCount", 5);
// 读取私有字段
int actualRetry = Whitebox.getInternalState(service, "maxRetryCount");
assertEquals(5, actualRetry);
// 调用私有方法
String result = Whitebox.invokeMethod(service, "buildOrderNumber", 1001L);
assertEquals("ORD-1001", result);
// 获取私有内部类实例
Object inner = Whitebox.getInternalState(service, "internalHandler");
}
}PowerMock 适用场景
| 场景 | 是否必须 PowerMock | 说明 |
|---|---|---|
| 静态方法 | 是(Mockito 3.4+ 已内建 mockStatic,可替代) | Mockito 3.4+ 的 Inline MockMaker 可以实现 |
| 构造方法 | 是 | Mockito 无法 Mock new 表达式 |
| 私有方法 | 是 | Mockito 不支持 Mock 私有方法 |
| final 类 / final 方法 | Mockito 2.1+ 可选 | Mockito 的 Inline MockMaker 可以 Mock final 类 |
系统类(System.currentTimeMillis() 等) | 是 | JDK 类需要 PowerMock 的特殊类加载器 |
注意事项
- JUnit 5 兼容性:PowerMock 对 JUnit 5 的支持有限,建议在 JUnit 5 项目中考虑使用 Mockito Inline MockMaker(3.4+)替代。
- 测试速度:PowerMock 使用自定义类加载器,会显著拖慢测试执行速度。
- 代码覆盖率:PowerMock 修改字节码可能导致 JaCoCo 等覆盖率工具误报。
- 不推荐过度使用:依赖 PowerMock 往往是代码设计需要重构的信号(如过多静态方法、私有方法未通过公开接口测试)。
- 版本兼容性:PowerMock 通常滞后于 Mockito 主版本更新,选择版本时需要确认兼容矩阵。
Mockito Inline MockMaker 替代方案
Mockito 3.4.0+ 内置了 Inline MockMaker,可以替代部分 PowerMock 功能。
// 需要 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
// 内容为:mock-maker-inline
@Test
void testStaticWithMockitoInline() {
try (MockedStatic<IdGenerator> mocked = mockStatic(IdGenerator.class)) {
mocked.when(IdGenerator::nextId).thenReturn(1001L);
long id = orderService.generateOrderId();
assertEquals(1001L, id);
mocked.verify(IdGenerator::nextId, times(1));
}
}
@Test
void testFinalClassWithMockitoInline() {
// final 类不需要额外配置
FinalClass finalClass = mock(FinalClass.class);
when(finalClass.doSomething()).thenReturn("mocked");
}WireMock
WireMock 是一个用于 HTTP 服务 Mock 的工具,通过在本地启动 HTTP 服务器模拟外部 API。
基础配置与启动
// JUnit 4 规则方式
@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);
// 或指定配置
@Rule
public WireMockRule wireMockRule = new WireMockRule(
WireMockConfiguration.options()
.port(8089)
.httpsPort(8443)
.notifier(new ConsoleNotifier(true))
);
// JUnit 5 扩展方式
@WireMockTest(httpPort = 8089)
class ApiClientTest {
// 直接使用静态 WireMock API
}存根配置 — stubFor
import static com.github.tomakehurst.wiremock.client.WireMock.*;
// 基本 GET 请求存根
stubFor(get(urlEqualTo("/api/users/1"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":1,\"name\":\"Alice\"}")
));
// 带查询参数的请求
stubFor(get(urlPathEqualTo("/api/users"))
.withQueryParam("page", equalTo("1"))
.withQueryParam("size", equalTo("20"))
.willReturn(aResponse()
.withStatus(200)
.withBodyFile("json/users-page1.json")
));
// POST 请求带请求体匹配
stubFor(post(urlEqualTo("/api/orders"))
.withHeader("Authorization", containing("Bearer "))
.withRequestBody(matchingJsonPath("$.userId"))
.withRequestBody(matchingJsonPath("$.amount", matching("[0-9]+")))
.willReturn(aResponse()
.withStatus(201)
.withHeader("Location", "/api/orders/1001")
.withBody("{\"orderId\":1001,\"status\":\"CREATED\"}")
));
// 延迟响应 — 模拟超时
stubFor(get(urlEqualTo("/api/slow"))
.willReturn(aResponse()
.withFixedDelay(5000) // 固定 5 秒延迟
));
// 模拟故障
stubFor(get(urlEqualTo("/api/unstable"))
.willReturn(aResponse()
.withStatus(503)
.withBody("Service Unavailable")
));
// 模拟连接超时(直接断开连接)
stubFor(get(urlEqualTo("/api/timeout"))
.willReturn(aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)
));URL 匹配方式:
| 匹配器 | 示例 | 说明 |
|---|---|---|
urlEqualTo(path) | /api/users?id=1 | 精确匹配完整 URL |
urlPathEqualTo(path) | /api/users | 仅匹配路径,忽略查询参数 |
urlMatching(regex) | /api/users/\\d+ | URL 正则匹配 |
urlPathMatching(regex) | /api/users/.* | 路径正则匹配 |
请求体匹配方式:
| 匹配器 | 说明 |
|---|---|
equalToJson(json) | JSON 精确匹配 |
matchingJsonPath(path) | JSON Path 存在性匹配 |
matchingJsonPath(path, matcher) | JSON Path 值匹配 |
equalToXml(xml) | XML 精确匹配 |
matchingXPath(path) | XPath 存在性匹配 |
containing(substring) | 子串包含匹配 |
matching(regex) | 正则匹配 |
notMatching(regex) | 正则不匹配 |
请求验证 — verify
// 验证请求被接收
verify(getRequestedFor(urlEqualTo("/api/users/1")));
// 验证特定次数
verify(2, postRequestedFor(urlEqualTo("/api/orders")));
// 验证请求头
verify(getRequestedFor(urlEqualTo("/api/users"))
.withHeader("Authorization", equalTo("Bearer token123")));
// 验证查询参数
verify(getRequestedFor(urlPathEqualTo("/api/search"))
.withQueryParam("q", equalTo("test")));
// 验证请求体
verify(postRequestedFor(urlEqualTo("/api/orders"))
.withRequestBody(equalToJson("{\"userId\":1}")));
// 验证没有收到请求
verify(0, getRequestedFor(urlEqualTo("/api/admin")));
// 获取所有匹配的请求,用于自定义断言
List<LoggedRequest> requests = findAll(getRequestedFor(urlMatching("/api/.*")));
assertEquals(3, requests.size());
requests.forEach(req -> {
assertTrue(req.getHeader("Authorization").startsWith("Bearer "));
});录制/回放模式
WireMock 可以在录制模式下作为代理运行,记录真实 API 的响应,然后在测试中回放。
// 以录制模式启动
// 方案一:编程启动
WireMockRule wireMockRule = new WireMockRule(
WireMockConfiguration.options()
.port(8089)
.proxyPassThrough(true) // 启用代理透传
.recordMappings(new Recorder.RecordSpec()
.forTarget("https://real-api.example.com")
.extractTextBodiesOver(1024)
.makePartialJsonMatchDisabled(true))
);
// 方案二:使用录制命令行
// java -jar wiremock-standalone.jar --proxy-all="https://real-api.example.com" --record
// 在测试中录制特定请求
@Before
public void startRecording() {
WireMock.startRecording("https://real-api.example.com");
}
@After
public void stopRecording() {
WireMock.stopRecording();
// 录制的映射文件保存在 __files 和 mappings 目录中
}
// 回放模式(默认)
// 只需启动 WireMock 并确保 mappings 目录下有录制的存根文件
// 启动时自动加载 mappings/*.json 中的存根配置录制的映射文件示例(mappings/user-api.json):
{
"request": {
"method": "GET",
"urlPath": "/api/users/1"
},
"response": {
"status": 200,
"jsonBody": { "id": 1, "name": "Alice" },
"headers": {
"Content-Type": "application/json"
}
},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"metadata": {
"recordedAt": "2024-01-15T10:30:00Z",
"originalUrl": "https://real-api.example.com/api/users/1"
}
}状态模拟 — 故障注入与超时
// 故障类型
// 1. 固定延迟
stubFor(get(urlEqualTo("/api/slow"))
.willReturn(aResponse().withFixedDelay(5000)));
// 2. 随机延迟(模拟网络抖动)
stubFor(get(urlEqualTo("/api/unstable"))
.willReturn(aResponse()
.withLogNormalRandomDelay(90, 0.1))); // 中位数 90ms,标准差 0.1
// 3. 连接重置
stubFor(get(urlEqualTo("/api/reset"))
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
// 4. 空响应
stubFor(get(urlEqualTo("/api/empty"))
.willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE)));
// 5. 随机数据
stubFor(get(urlEqualTo("/api/garbage"))
.willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE)));
// 6. HTTP 错误状态码
stubFor(get(urlEqualTo("/api/not-found"))
.willReturn(aResponse().withStatus(404).withBody("Not Found")));
stubFor(get(urlEqualTo("/api/error")))
.willReturn(aResponse().withStatus(500).withBody("Internal Error")));
stubFor(get(urlEqualTo("/api/rate-limited")))
.willReturn(aResponse()
.withStatus(429)
.withHeader("Retry-After", "60")
.withBody("Too Many Requests")));
// 7. 动态响应,基于请求内容决策
stubFor(post(urlEqualTo("/api/payment"))
.willReturn(aResponse()
.withTransformers("response-template")
.withBody("{\"status\": \"{{request.body.[?(@.amount > 10000)].status}}\"}")
));WireMock 的模拟服务器管理
// 编程管理
// 启动
WireMockServer wireMockServer = new WireMockServer(8089);
wireMockServer.start();
// 重置所有存根和记录
wireMockServer.resetAll();
// 使用 JSON 文件加载存根
WireMock.loadMappingsFrom("mappings");
// 重置特定存根
WireMock.resetToDefaultMappings();
// 优雅关闭
wireMockServer.stop();Spring Boot 集成
@MockBean 与 @SpyBean
Spring Boot Test 提供了 @MockBean 和 @SpyBean 注解,可以将 Mock/Spy 对象注入到 Spring 容器中,替换原有的 Bean。
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // 替换容器中的 UserRepository Bean
private UserRepository userRepository;
@SpyBean // 替换容器中的 AuditService Bean,保留真实逻辑
private AuditService auditService;
@Test
void testGetUser() throws Exception {
// 配置 Mock Bean 行为
User mockUser = new User(1L, "Alice", "alice@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// 执行 HTTP 请求
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
// Spy Bean 的真实方法会被执行
verify(auditService).logAccess(anyString());
}
}@MockBean 与 @SpyBean 对比:
| 特性 | @MockBean | @SpyBean |
|---|---|---|
| 默认行为 | 所有方法返回默认值 | 调用真实方法 |
| 状态 | 全新 Mock 对象 | 基于真实实例包装 |
| 应用场景 | 完全替换外部依赖 | 部分覆盖,增强已有逻辑 |
| 容器影响 | 覆盖容器中的同名 Bean | 用 Spy 包装后替换 |
注意事项:
@MockBean/@SpyBean会导致 ApplicationContext 缓存失效,每次使用不同 Mock 组合的测试类都会重建容器,影响测试速度。- 对于单个测试类的多个测试方法,Mock Bean 的状态会共享,建议在每个测试方法前使用
Mockito.reset()重置。
@BeforeEach
void setUp() {
Mockito.reset(userRepository, auditService);
}MockMvc + Mockito 配合
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void testCreateOrder() throws Exception {
OrderCreateRequest request = new OrderCreateRequest();
request.setUserId(1L);
request.setItemId(100L);
request.setQuantity(2);
OrderResponse response = new OrderResponse();
response.setOrderId(1001L);
response.setStatus("CREATED");
when(orderService.createOrder(any(OrderCreateRequest.class)))
.thenReturn(response);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"userId\":1,\"itemId\":100,\"quantity\":2}")
.header("Authorization", "Bearer test-token"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.orderId").value(1001))
.andExpect(jsonPath("$.status").value("CREATED"));
verify(orderService).createOrder(any(OrderCreateRequest.class));
}
@Test
void testCreateOrderValidationFailure() throws Exception {
// 测试 Bean Validation 注解
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"userId\":null,\"itemId\":null,\"quantity\":0}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray());
}
@Test
void testGetOrderNotFound() throws Exception {
when(orderService.getOrderById(999L))
.thenThrow(new OrderNotFoundException(999L));
mockMvc.perform(get("/api/orders/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("Order not found: 999"));
}
}WireMock @AutoConfigureWireMock
Spring Boot 通过 @AutoConfigureWireMock 注解集成 WireMock,方便在集成测试中模拟外部 HTTP 服务。
@SpringBootTest
@AutoConfigureWireMock(port = 8089) // 启动 WireMock 在 8089 端口
class PaymentServiceIntegrationTest {
@Autowired
private PaymentService paymentService;
@BeforeEach
void setUp() {
// 配置 WireMock 存根
stubFor(post(urlEqualTo("/payment/charge"))
.withRequestBody(matchingJsonPath("$.amount"))
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"transactionId\":\"txn-001\",\"status\":\"SUCCESS\"}")
.withHeader("Content-Type", "application/json")
));
}
@Test
void testPaymentSuccess() {
PaymentRequest request = new PaymentRequest();
request.setAmount(BigDecimal.valueOf(99.99));
request.setCurrency("CNY");
PaymentResult result = paymentService.charge(request);
assertEquals("txn-001", result.getTransactionId());
assertEquals(PaymentStatus.SUCCESS, result.getStatus());
}
@Test
void testPaymentTimeout() {
// 模拟支付网关超时
stubFor(post(urlEqualTo("/payment/charge"))
.willReturn(aResponse()
.withFixedDelay(30000) // 30 秒延迟
));
assertThrows(PaymentTimeoutException.class, () -> {
paymentService.charge(request);
});
}
@Test
void testPaymentRetryOnFailure() {
// 先失败两次,第三次成功
stubFor(post(urlEqualTo("/payment/charge"))
.inScenario("Retry Scenario")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(aResponse().withStatus(503))
.willSetStateTo("First Retry"));
stubFor(post(urlEqualTo("/payment/charge"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("First Retry")
.willReturn(aResponse().withStatus(503))
.willSetStateTo("Second Retry"));
stubFor(post(urlEqualTo("/payment/charge"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("Second Retry")
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"transactionId\":\"txn-002\",\"status\":\"SUCCESS\"}")
));
PaymentResult result = paymentService.charge(request);
assertEquals("txn-002", result.getTransactionId());
}
}高级配置选项:
@SpringBootTest
@AutoConfigureWireMock(
port = 0, // 随机端口(通过 WireMockConfiguration 获取实际端口)
stubs = "classpath:wiremock/stubs", // 存根文件位置
files = "classpath:wiremock/__files" // 响应文件位置
)
class AdvancedWireMockTest {
@Autowired
private WireMockServer wireMockServer;
@Value("${wiremock.server.port}")
private int wireMockPort;
@BeforeEach
void setUp() {
// 动态配置被测试服务的 Base URL
paymentService.setBaseUrl("http://localhost:" + wireMockPort);
}
}Mockito vs PowerMock vs WireMock 对比
定位与核心能力
| 对比维度 | Mockito | PowerMock | WireMock |
|---|---|---|---|
| 定位 | 对象级别的行为 Mock | Mockito 的增强扩展 | HTTP 服务级别的 Mock |
| Mock 对象 | Java 对象 / 接口 | Java 对象 + 静态/构造/私有 | HTTP 服务端点 |
| 运行方式 | 字节码代理(JDK Proxy / CGLIB / ByteBuddy) | 自定义类加载器 + 字节码修改 | 独立 HTTP 服务器 |
| 适用范围 | 单元测试、层内集成测试 | 遗留代码、不可修改的第三方库 | 微服务间调用、外部 API 集成测试 |
| 是否需要启动服务器 | 否 | 否 | 是 |
可 Mock 的能力
| 能力 | Mockito | Mockito Inline (3.4+) | PowerMock | WireMock |
|---|---|---|---|---|
| 接口 | 支持 | 支持 | 支持 | 不适用 |
| 普通类 | 支持 | 支持 | 支持 | 不适用 |
| final 类 | 不支持(Inline 支持) | 支持 | 支持 | 不适用 |
| final 方法 | 不支持(Inline 支持) | 支持 | 支持 | 不适用 |
| 静态方法 | 不支持(Inline 支持) | 支持 | 支持 | 不适用 |
| 私有方法 | 不支持 | 不支持 | 支持 | 不适用 |
| 构造方法 | 不支持 | 不支持 | 支持 | 不适用 |
new 表达式 | 不支持 | 不支持 | 支持 | 不适用 |
| HTTP 请求 | 不支持 | 不支持 | 不支持 | 支持 |
| 连接超时/重置 | 不支持 | 不支持 | 不支持 | 支持 |
| 响应延迟 | 不支持 | 不支持 | 不支持 | 支持 |
测试策略建议
| 测试类型 | 推荐 Mock 方案 | 说明 |
|---|---|---|
| 单元测试(Service 层) | Mockito | Mock DAO/Repository,专注业务逻辑 |
| 单元测试(Controller 层) | Mockito + MockMvc | Mock Service,测试 HTTP 映射与序列化 |
| 数据访问层测试 | 不使用 Mock | 使用 H2/Testcontainers 真实数据库 |
| 含静态/私有方法的遗留代码 | Mockito Inline / PowerMock | 尽量重构,PowerMock 作为最后手段 |
| 外部 API 集成测试 | WireMock | 模拟第三方服务,保证测试稳定性和速度 |
| 契约测试 | WireMock + Spring Cloud Contract | 基于 WireMock 的 Stub 实现消费者驱动契约 |
| 端到端测试 | 不使用 Mock | 部署完整测试环境 |
性能与维护
| 指标 | Mockito | PowerMock | WireMock |
|---|---|---|---|
| 测试执行速度 | 快 | 慢(类加载器) | 中(HTTP 开销) |
| 配置复杂度 | 低 | 中高 | 中 |
| JUnit 5 支持 | 原生 | 有限 | 原生 |
| 与 Spring Boot 集成 | @MockBean / @SpyBean | 复杂 | @AutoConfigureWireMock |
| 代码侵入性 | 无 | 需要 @PrepareForTest | 配置 HTTP Base URL |
| 学习曲线 | 低 | 高 | 中 |
| 推荐使用度 | 高(首选) | 低(最后手段) | 高(HTTP Mock 首选) |
选型决策流程
需要 Mock 什么?
├── Java 对象 / 接口 → Mockito(首选)
│ ├── 需要 Mock 静态方法 → Mockito Inline (3.4+) / PowerMock
│ ├── 需要 Mock 构造方法 → PowerMock
│ ├── 需要 Mock 私有方法 → PowerMock
│ └── 需要 Mock final 类 → Mockito Inline / PowerMock
├── HTTP 服务 → WireMock
│ ├── 需要录制真实 API → WireMock 录制模式
│ ├── 需要故障注入 → WireMock Fault
│ └── 需要多场景编排 → WireMock Scenario
└── 混合场景(对象 + HTTP)
├── 业务层对象 → Mockito
└── 外部 HTTP 依赖 → WireMock总结:日常单元测试优先使用 Mockito,Spring Boot 集成测试使用 Mockito + MockMvc + WireMock,PowerMock 仅作为处理遗留代码的最后手段,并优先考虑代码重构而非引入 PowerMock。