title: 使用 MockServer 测试 REST API 集成 linkTitle: MockServer description: 了解如何使用 Testcontainers 的 MockServer 模块测试 Spring Boot 应用中的 REST API 集成。 keywords: testcontainers, java, spring boot, testing, mockserver, rest api, rest assured summary: | 了解如何创建一个与外部 REST API 集成的 Spring Boot 应用,然后使用 Testcontainers 和 MockServer 对这些集成进行测试。 aliases:
- /guides/testcontainers-java-mockserver/create-project/
- /guides/testcontainers-java-mockserver/run-tests/
- /guides/testcontainers-java-mockserver/write-tests/ params: tags: [testing] time: 20 minutes
在本指南中,你将学习如何:
- 创建一个与外部 REST API 通信的 Spring Boot 应用
- 使用 Testcontainers 的 MockServer 模块测试外部 API 集成
先决条件
- Java 17+
- Maven 或 Gradle
- 一个受 Testcontainers 支持的 Docker 环境
Note如果你刚接触 Testcontainers,请访问 Testcontainers 概览 了解 Testcontainers 及其使用优势。
创建 Spring Boot 项目
搭建项目
在 Spring Initializr 上创建一个 Spring Boot 项目, 添加 Spring Web、Spring Reactive Web 以及 Testcontainers 这几个 starter。
或者,克隆 指南仓库。
生成项目后,添加 REST Assured 和 MockServer 库作为测试依赖。pom.xml 中的
关键依赖如下:
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mockserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>建议使用 Testcontainers 的 BOM(Bill of Materials,物料清单),这样你无需为每个 Testcontainers 模块依赖重复指定版本。
本指南构建的是一个管理视频相册(album)的应用。相册中的照片资源由第三方 REST API 处理。出于演示目的,该应用使用公开可用的 JSONPlaceholder API 作为照片服务。
该应用暴露一个 GET /api/albums/{albumId} 端点,它会调用照片服务以获取指定相册的
照片。MockServer 是一个用于 mock 基于 HTTP 的服务
的库。Testcontainers 提供了一个 MockServer 模块,
能以 Docker 容器方式运行 MockServer。
创建 Album 和 Photo 模型
使用 Java record 创建 Album.java:
package com.testcontainers.demo;
import java.util.List;
public record Album(Long albumId, List<Photo> photos) {}
record Photo(Long id, String title, String url, String thumbnailUrl) {}创建 PhotoServiceClient 接口
Spring Framework 6 引入了声明式 HTTP 客户端支持。 创建一个接口,包含一个按相册 ID 获取照片的方法:
package com.testcontainers.demo;
import java.util.List;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
interface PhotoServiceClient {
@GetExchange("/albums/{albumId}/photos")
List<Photo> getPhotos(@PathVariable Long albumId);
}将 PhotoServiceClient 注册为 bean
要生成 PhotoServiceClient 的运行时实现,需使用 HttpServiceProxyFactory 将其
注册为 Spring bean。该工厂需要一个 HttpClientAdapter 实现。Spring Boot 提供了
WebClientAdapter 作为 spring-webflux 库的一部分:
package com.testcontainers.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Configuration
public class AppConfig {
@Bean
public PhotoServiceClient photoServiceClient(
@Value("${photos.api.base-url}") String photosApiBaseUrl
) {
WebClient client = WebClient.builder().baseUrl(photosApiBaseUrl).build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(client))
.build();
return factory.createClient(PhotoServiceClient.class);
}
}照片服务的基础 URL 被外部化为一个配置属性。在 src/main/resources/application.properties
中添加以下条目:
photos.api.base-url=https://jsonplaceholder.typicode.com创建 REST API 端点
创建 AlbumController.java:
package com.testcontainers.demo;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClientResponseException;
@RestController
@RequestMapping("/api")
class AlbumController {
private static final Logger logger = LoggerFactory.getLogger(
AlbumController.class
);
private final PhotoServiceClient photoServiceClient;
AlbumController(PhotoServiceClient photoServiceClient) {
this.photoServiceClient = photoServiceClient;
}
@GetMapping("/albums/{albumId}")
public ResponseEntity<Album> getAlbumById(@PathVariable Long albumId) {
try {
List<Photo> photos = photoServiceClient.getPhotos(albumId);
return ResponseEntity.ok(new Album(albumId, photos));
} catch (WebClientResponseException e) {
logger.error("Failed to get photos", e);
return new ResponseEntity<>(e.getStatusCode());
}
}
}该端点会针对给定的相册 ID 调用照片服务,并返回类似如下的响应:
{
"albumId": 1,
"photos": [
{
"id": 51,
"title": "non sunt voluptatem placeat consequuntur rem incidunt",
"url": "https://via.placeholder.com/600/8e973b",
"thumbnailUrl": "https://via.placeholder.com/150/8e973b"
},
{
"id": 52,
"title": "eveniet pariatur quia nobis reiciendis laboriosam ea",
"url": "https://via.placeholder.com/600/121fa4",
"thumbnailUrl": "https://via.placeholder.com/150/121fa4"
}
]
}使用 Testcontainers MockServer 编写测试
在 HTTP 协议层面(而非 mock Java 方法)来 mock 外部 API 的交互,让你可以验证序列化与 反序列化行为,并模拟网络故障。
Testcontainers 提供了一个 MockServer 模块,可在 Docker 容器内启动一个
MockServer 实例。然后你可以使用 MockServerClient
配置 mock 预期(expectation)。
编写测试
创建 AlbumControllerTest.java:
package com.testcontainers.demo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.Header;
import org.mockserver.verify.VerificationTimes;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.mockserver.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class AlbumControllerTest {
@LocalServerPort
private Integer port;
@Container
static MockServerContainer mockServerContainer =
new MockServerContainer("mockserver/mockserver:5.15.0");
static MockServerClient mockServerClient;
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
mockServerClient =
new MockServerClient(
mockServerContainer.getHost(),
mockServerContainer.getServerPort()
);
registry.add("photos.api.base-url", mockServerContainer::getEndpoint);
}
@BeforeEach
void setUp() {
RestAssured.port = port;
mockServerClient.reset();
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
mockServerClient
.when(
request().withMethod("GET").withPath("/albums/" + albumId + "/photos")
)
.respond(
response()
.withStatusCode(200)
.withHeaders(
new Header("Content-Type", "application/json; charset=utf-8")
)
.withBody(
json(
"""
[
{
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
}
]
"""
)
)
);
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
verifyMockServerRequest("GET", "/albums/" + albumId + "/photos", 1);
}
@Test
void shouldReturn404StatusWhenAlbumNotFound() {
Long albumId = 1L;
mockServerClient
.when(
request().withMethod("GET").withPath("/albums/" + albumId + "/photos")
)
.respond(response().withStatusCode(404));
given()
.contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(404);
verifyMockServerRequest("GET", "/albums/" + albumId + "/photos", 1);
}
private void verifyMockServerRequest(String method, String path, int times) {
mockServerClient.verify(
request().withMethod(method).withPath(path),
VerificationTimes.exactly(times)
);
}
}该测试的作用如下:
@SpringBootTest在随机端口上启动完整的应用。@Testcontainers与@Container注解启动一个MockServerContainer,并创建连接到 它的MockServerClient。@DynamicPropertySource覆盖photos.api.base-url,使其指向 MockServer 端点,从而 让应用与 MockServer 通信,而非真实的照片服务。@BeforeEach在每个测试之前重置MockServerClient,使一个测试的预期不会影响 另一个测试。shouldGetAlbumById()为/albums/{albumId}/photos配置一个 mock 响应,向应用的/api/albums/{albumId}端点发送请求,并校验响应体。它还使用mockServerClient.verify()确认预期的 API 调用确实到达了 MockServer。shouldReturn404StatusWhenAlbumNotFound()配置 MockServer 返回 404 状态,并验证应用 将该状态传播给调用方。
运行测试与后续步骤
运行测试
$ ./mvnw test
或者使用 Gradle:
$ ./gradlew test
你应该在控制台输出中看到 MockServer 的 Docker 容器启动。它充当照片服务,依据配置的 预期(expectation)返回 mock 响应。所有测试都应通过。
小结
你构建了一个使用声明式 HTTP 客户端与外部 REST API 集成的 Spring Boot 应用,然后使用 Testcontainers 的 MockServer 模块对该集成进行了测试。在 HTTP 协议层面而非 mock Java 方法层面进行测试,能让你发现序列化问题并模拟真实的故障场景。
想进一步了解 Testcontainers,请访问 Testcontainers 概览。