1. 引言

在 Spring Boot 应用的单元测试中,我们经常需要测试那些依赖外部 HTTP 服务的组件。直接调用真实的外部服务会带来网络延迟、服务不稳定、测试数据不可控等问题。这时,为 RestTemplateWebClient 等 HTTP 客户端设置 Mock 行为就成为了最佳实践。

本文将详细介绍如何根据你的业务需求,为 RestTemplate 或 HTTP 客户端设置 Mock 行为,确保单元测试的隔离性、可重复性和高效性。

2. 为什么需要 Mock HTTP 客户端?

在单元测试中,我们关注的是被测组件自身的逻辑是否正确,而不是外部服务的稳定性。Mock HTTP 客户端可以带来以下好处:

  • 测试隔离:不依赖外部网络和服务状态。
  • 可控的测试数据:可以模拟各种成功、失败、超时等场景。
  • 测试速度:避免了网络请求的耗时。
  • 可重复性:每次测试都能获得相同的响应。

3. 为 RestTemplate 设置 Mock 行为

RestTemplate 是 Spring 传统的同步 HTTP 客户端。我们可以使用 MockRestServiceServer 来 Mock 其行为。

3.1 添加依赖

确保你的项目中包含了 Spring Boot Test 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3.2 基本 Mock 示例

以下是一个完整的单元测试示例,展示了如何为 RestTemplate 设置 Mock 行为:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;

@SpringBootTest
public class UserServiceTest {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testGetUserById() {
        // 创建 Mock 服务器
        MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
        
        // 设置 Mock 行为:当请求 GET /users/123 时,返回成功的 JSON 响应
        mockServer.expect(requestTo("http://api.example.com/users/123"))
                  .andExpect(method(HttpMethod.GET))
                  .andRespond(withSuccess(
                      "{\"id\":123,\"name\":\"张三\",\"email\":\"zhangsan@example.com\"}",
                      MediaType.APPLICATION_JSON
                  ));
        
        // 执行被测方法
        User user = userService.getUserById(123);
        
        // 验证 Mock 请求是否按预期执行
        mockServer.verify();
        
        // 断言业务逻辑
        assertThat(user.getId()).isEqualTo(123);
        assertThat(user.getName()).isEqualTo("张三");
    }
    
    @Test
    public void testGetUserById_NotFound() {
        MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
        
        // 模拟 404 响应
        mockServer.expect(requestTo("http://api.example.com/users/999"))
                  .andExpect(method(HttpMethod.GET))
                  .andRespond(withStatus(HttpStatus.NOT_FOUND));
        
        // 执行并验证异常
        assertThrows(UserNotFoundException.class, () -> {
            userService.getUserById(999);
        });
        
        mockServer.verify();
    }
}

3.3 高级 Mock 配置

你还可以根据业务需求设置更复杂的 Mock 行为:

// 1. 模拟延迟响应
mockServer.expect(requestTo("/api/data"))
          .andExpect(method(HttpMethod.GET))
          .andRespond(withSuccess()
              .body("{\"status\":\"processing\"}")
              .contentType(MediaType.APPLICATION_JSON)
              .delay(Duration.ofSeconds(2))); // 2秒延迟

// 2. 验证请求头
mockServer.expect(requestTo("/api/protected"))
          .andExpect(method(HttpMethod.GET))
          .andExpect(header("Authorization", "Bearer token123"))
          .andRespond(withSuccess());

// 3. 验证请求体(POST/PUT)
mockServer.expect(requestTo("/api/users"))
          .andExpect(method(HttpMethod.POST))
          .andExpect(content().json("{\"name\":\"李四\",\"age\":30}"))
          .andRespond(withCreatedEntity(URI.create("/api/users/456")));

// 4. 模拟网络错误
mockServer.expect(requestTo("/api/unstable"))
          .andExpect(method(HttpMethod.GET))
          .andRespond(withException(new IOException("Connection timeout")));

4. 为 WebClient 设置 Mock 行为

对于响应式的 WebClient,我们可以使用 MockWebServer(来自 OkHttp)或 WireMock

4.1 使用 MockWebServer

import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.MockResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.test.StepVerifier;

public class ReactiveUserServiceTest {
    
    private MockWebServer mockWebServer;
    private WebClient webClient;
    
    @BeforeEach
    void setUp() throws IOException {
        mockWebServer = new MockWebServer();
        mockWebServer.start();
        
        webClient = WebClient.builder()
                .baseUrl(mockWebServer.url("/").toString())
                .build();
    }
    
    @AfterEach
    void tearDown() throws IOException {
        mockWebServer.shutdown();
    }
    
    @Test
    void testGetUser() {
        // 设置 Mock 响应
        mockWebServer.enqueue(new MockResponse()
                .setResponseCode(200)
                .setHeader("Content-Type", "application/json")
                .setBody("{\"id\":1,\"name\":\"王五\"}"));
        
        // 执行请求
        Mono<User> userMono = webClient.get()
                .uri("/users/1")
                .retrieve()
                .bodyToMono(User.class);
        
        // 验证
        StepVerifier.create(userMono)
                .expectNextMatches(user -> 
                    user.getId() == 1 && "王五".equals(user.getName()))
                .verifyComplete();
        
        // 验证请求
        RecordedRequest request = mockWebServer.takeRequest();
        assertThat(request.getPath()).isEqualTo("/users/1");
        assertThat(request.getMethod()).isEqualTo("GET");
    }
}

4.2 使用 WireMock

WireMock 是一个更强大的 HTTP Mock 服务器,支持更复杂的场景:

import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;

import static com.github.tomakehurst.wiremock.client.WireMock.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 8089)
public class WireMockUserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testGetUserWithWireMock() {
        // 设置 WireMock 桩
        stubFor(get(urlEqualTo("/users/100"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\"id\":100,\"name\":\"赵六\"}")));
        
        // 执行测试
        User user = userService.getUserById(100);
        
        // 验证
        assertThat(user.getId()).isEqualTo(100);
        assertThat(user.getName()).isEqualTo("赵六");
        
        // 验证请求是否发送到 WireMock
        verify(getRequestedFor(urlEqualTo("/users/100")));
    }
}

5. 最佳实践与注意事项

5.1 根据业务场景选择 Mock 策略

  • 简单内部服务调用:使用 MockRestServiceServer(RestTemplate)或 MockWebServer(WebClient)。
  • 复杂外部 API 模拟:使用 WireMock,支持请求匹配、响应模板、状态机等高级功能。
  • 契约测试:考虑使用 Spring Cloud Contract,确保消费者和提供者之间的契约一致性。

5.2 测试数据管理

将测试数据外部化,便于维护:

// 使用 @Value 或资源文件加载测试数据
@Value("classpath:test-data/user-success.json")
private Resource userSuccessResource;

@Test
public void testWithExternalData() throws IOException {
    String responseBody = new String(userSuccessResource.getInputStream().readAllBytes());
    
    mockServer.expect(requestTo("/api/users/1"))
              .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
    
    // ... 测试逻辑
}

5.3 清理与重置

在每个测试方法后清理 Mock 状态,避免测试间相互影响:

@AfterEach
public void tearDown() {
    // 对于 MockRestServiceServer
    if (mockServer != null) {
        mockServer.verify();
        mockServer.reset(); // 重置期望
    }
    
    // 对于 WireMock
    WireMock.reset();
}

5.4 验证请求次数

确保 HTTP 客户端按预期次数调用:

// 验证只调用一次
mockServer.expect(once(), requestTo("/api/data"))
          .andRespond(withSuccess());

// 验证调用两次
mockServer.expect(times(2), requestTo("/api/data"))
          .andRespond(withSuccess());

// 验证最多调用一次
mockServer.expect(atMostOnce(), requestTo("/api/data"))
          .andRespond(withSuccess());

6. 常见问题与解决方案

6.1 Mock 不生效?

可能原因

  1. RestTemplate/WebClient 不是通过 Spring 容器注入的(自己 new 的实例)
  2. 测试类没有正确的注解(如 @SpringBootTest
  3. URL 不匹配(注意协议、域名、端口、路径)

解决方案

// 确保使用同一个 RestTemplate 实例
@Configuration
public class TestConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

// 在测试中注入
@Autowired
private RestTemplate restTemplate;

6.2 如何 Mock 第三方库内部的 HTTP 调用?

如果第三方库内部创建了自己的 HTTP 客户端,可以考虑:

  1. 使用 PowerMock/Mockito 来 Mock 静态方法或构造函数
  2. 将第三方库包装一层,测试自己的包装类
  3. 使用真正的 Mock 服务器(如 WireMock)并配置代理

6.3 性能测试中的 Mock

对于性能测试,简单的 Mock 可能不够:

  • 使用 MockRestServiceServerandRespond() 方法设置延迟
  • 使用 WireMock 的 fixedDelay()randomDelay()
  • 考虑使用专门的性能测试工具(如 Gatling、JMeter)

7. 总结

根据你的业务需求为 HTTP 客户端设置 Mock 行为,是编写高质量单元测试的关键。本文介绍了:

  • 使用 MockRestServiceServer Mock RestTemplate
  • 使用 MockWebServer 或 WireMock Mock WebClient
  • 各种业务场景下的 Mock 配置示例
  • 最佳实践和常见问题解决方案

选择适合你项目的 Mock 策略,可以让单元测试更加可靠、快速和可维护。记住:好的 Mock 不是让测试通过,而是让测试真实反映业务逻辑的正确性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐