用 WireMock 和 MockServer 隔离 Micronaut 外部 REST 依赖
原文:Sergio del Amo,Testing REST API integrations using Testcontainers with WireMock or MockServer(Java / Gradle)。中文翻译与技术整理:未完纪。本文保留同页内嵌 WireMock、WireMock 容器和 MockServer 三种方案;它们是同一集成测试任务的替代实现。
本文在 2026 年 10 月 5 日读完整篇指南,并下载 官方随文源码包静态核对。下载包 gradle.properties 指定 Micronaut 5.2.0,build.gradle 的 sourceCompatibility 和 targetCompatibility 均为 Java 25。正文仍写 JDK 21 或更新,因此使用当前源码包应准备 JDK 25 并正确配置 JAVA_HOME,不能只按旧前提选 JDK 21。还需要编辑器以及 Testcontainers 支持的 Docker 环境。本轮未构建、启动或运行测试。

要验证的是跨 HTTP 的集成
假设应用管理视频相册,照片与视频资产由第三方 REST 服务提供。教程使用公开的 JSONPlaceholder 作为照片服务。我们的接口接收 albumId,在内部读取该相册的照片,再把 albumId 和照片列表组成 Album 响应。
直接模拟 photoServiceClient.getPhotos(albumId) 只能检查局部调用;在 HTTP 层替换上游,实际客户端的 URL、请求、序列化与反序列化也会进入测试路径,并能进一步构造延迟等场景。不过,本页实际测试主要覆盖固定成功响应、上游 500 和空结果;不能据此宣称已覆盖网络超时、重试或真实上游契约。
创建应用与数据模型
用 Micronaut CLI 建立 Java、Gradle、JUnit 项目,启用 http-client、micronaut-test-rest-assured 和 testcontainers。也可以在 Micronaut Launch 中选择相同特性。
mn create-app example.micronaut.micronautguide \
--features=http-client,micronaut-test-rest-assured,testcontainers \
--build=gradle \
--lang=java \
--test=junit
该命令在 micronautguide 目录创建默认包 example.micronaut。原文说明,省略 –build 默认使用 Gradle Kotlin DSL;省略语言默认 Java,Java/Kotlin 默认 JUnit,Groovy 默认 Spock。本篇固定使用页面选定的 Java / Gradle 变体,现有项目可对照 Launch 的特性差异合并依赖和配置。
首先创建 Photo.java 与 Album.java。Java record 表达数据结构,@Serdeable 允许 Micronaut 为类型生成序列化和反序列化支持。
package example.micronaut;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record Photo(Long id, String title, String url, String thumbnailUrl) {
}
package example.micronaut;
import io.micronaut.serde.annotation.Serdeable;
import java.util.List;
@Serdeable
public record Album(Long albumId, List<Photo> photos) {
}
把外部地址做成配置
PhotoServiceClient 是声明式 HTTP 客户端。@Client(id = “photosapi”) 使用服务标识符,@Get 定义上游路径,albumId 作为路径变量。接口返回 List<Photo>,客户端调用为同步读取。
package example.micronaut;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.client.annotation.Client;
import java.util.List;
@Client(id = "photosapi")
interface PhotoServiceClient {
@Get("/albums/{albumId}/photos")
List<Photo> getPhotos(@PathVariable Long albumId);
}
不要在测试时更改业务类来切换地址。在 src/main/resources/application.properties 中配置真实上游,测试则覆盖同一个属性,将请求指向本地模拟服务。
micronaut.http.services.photosapi.url=https://jsonplaceholder.typicode.com
AlbumController 接收 GET /api/albums/{albumId},通过构造函数注入客户端,并组合结果。同步网络 I/O 不能占用事件循环,因此方法上使用 @ExecuteOn(BLOCKING) 让它在阻塞任务执行器上运行。
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.scheduling.annotation.ExecuteOn;
import static io.micronaut.scheduling.TaskExecutors.BLOCKING;
@Controller("/api")
class AlbumController {
private final PhotoServiceClient photoServiceClient;
AlbumController(PhotoServiceClient photoServiceClient) {
this.photoServiceClient = photoServiceClient;
}
@ExecuteOn(BLOCKING)
@Get("/albums/{albumId}")
public Album getAlbumById(@PathVariable Long albumId) {
return new Album(albumId, photoServiceClient.getPhotos(albumId));
}
}
实际调用的上游路径为 https://jsonplaceholder.typicode.com/albums/{albumId}/photos。启动自己的应用后,可访问 http://localhost:8080/api/albums/1 观察结构。以下原文响应片段包含省略号,用来展示结构,不是可直接解析的完整 JSON,更不是我们本轮取得的线上数据。
{
"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"
},
...
...
]
}
方案一:用内嵌 WireMock 扩展测试
在 build.gradle 的 dependencies 中加入 WireMock Standalone。当前可读页面与下载源码包使用 3.0.4;本轮首次网页抓取曾出现 @wiremock-standaloneVersion@ 未展开占位符,以下以实际源码包核对后的值为准。该版本是教程快照,不是本文对最新版本或安全性的推荐。
testImplementation("org.wiremock:wiremock-standalone:3.0.4")
原文同时要求把 https://jitpack.io 加入仓库列表;下载包也包含该仓库,后面的 WireMock Testcontainers 包使用它。这里保留上游项目的依赖设定,没有执行依赖解析或评估替换版本。
版本兼容性边界:源码包固定使用 com.github.wiremock:wiremock-testcontainers-java:1.0-alpha-6,而 org.testcontainers 依赖未写版本。Micronaut Platform 5.2.0 的管理表列出 Testcontainers 2.0.5;WireMock Testcontainers 上游兼容说明指出,早于 1.0-alpha-16 的版本按 Testcontainers 2 更新前的集成测试路线维护,新版本才切到 Testcontainers 2,并要求 JUnit 5。现有资料不足以确认 alpha-6 与此项目最终解析到的 Testcontainers 版本兼容;不要把这组依赖当作已经验证的可复制组合。采用前请对照上游兼容说明,锁定一致的模块版本并在目标环境执行测试。本稿保留 alpha-6 作为源码快照事实,未解析依赖、构建或运行测试。
下面是 AlbumControllerTest.java。WireMockExtension 选择随机端口,getProperties() 把 photosapi.url 指向扩展的 baseUrl。每个用例用 try-with-resources 启动 EmbeddedServer,离开作用域后关闭它;RestAssured.port 使用实际应用端口。
package example.micronaut;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.MediaType;
import io.micronaut.runtime.server.EmbeddedServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Collections;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
@Testcontainers(disabledWithoutDocker = true)
class AlbumControllerTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
private Map<String, Object> getProperties() {
return Collections.singletonMap("micronaut.http.services.photosapi.url",
wireMock.baseUrl());
}
@Test
void shouldGetAlbumById() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
Long albumId = 1L;
wireMock.stubFor(
WireMock.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(
aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody(
"""
[
{
"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));
}
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
Long albumId = 2L;
wireMock.stubFor(WireMock.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(aResponse().withStatus(500)));
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
}
}
第一个用例把 GET /albums/1/photos 配成返回两个照片对象,再请求应用自身的 /api/albums/1,断言 HTTP 200、albumId 等于 1、photos 长度为 2。第二个用例把相册 2 的上游响应设为 500,并断言应用也返回 500。这里验证的是当前简单控制器的错误传播方式,尚未实现重试、降级或统一错误响应。
跳过条件:原文给这个内嵌 WireMock 类也加上了 @Testcontainers(disabledWithoutDocker = true)。WireMockExtension 本身可以在 JVM 内运行,但保留这条注解会让没有 Docker 的环境跳过本类测试。报告中必须区分通过与跳过,不能看到构建结束就判断集成测试已执行。
用 JSON 文件维护桩响应
WireMock 的响应也可以从资源文件加载。原文在此引入 WireMock Testcontainers 集成依赖,下载包采用 1.0-alpha-6:
testImplementation("com.github.wiremock:wiremock-testcontainers-java:1.0-alpha-6")
创建 src/test/resources/wiremock/mappings/get-album-photos.json,内容如下。第一条以正则匹配任意数字相册并返回文件中的成功响应;第二条给相册2返回500;第三条给相册3返回空数组。
{
"mappings": [
{
"request": {
"method": "GET",
"urlPattern": "/albums/([0-9]+)/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "album-photos-resp-200.json"
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/2/photos"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/3/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": []
}
}
]
}
bodyFileName 对应 WireMock 资源根下的 __files 目录。官方源码包中实际文件是 src/test/resources/wiremock/__files/album-photos-resp-200.json,内容如下。图片 URL 只是测试响应中的字符串,测试不会因为它们存在就下载图片,也不能据此认为这些图片服务当前可用。
[
{
"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"
}
]
WireMockExtension 改用 usingFilesUnderClasspath(“wiremock”),从这一资源根加载映射与响应体。再以同样方式覆盖服务地址、启动应用和断言返回。
@RegisterExtension
static WireMockExtension wireMockServer = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort().usingFilesUnderClasspath("wiremock"))
.build();
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
}
上面方法片段使用的 getProperties() 应返回 wireMockServer.baseUrl(),与这一节变量名一致。通用正则同时也匹配相册2和3,原文依赖映射的匹配优先行为;为让测试意图更明确,可以在自己的派生映射中为特定错误/空结果设置更高优先级,或把普通成功映射改为只匹配相册1。后一种最小修订如下,避免靠加载顺序决定本例三个固定用例的行为。此修订只改变测试匹配范围,不改变业务代码。
"request": {
"method": "GET",
"url": "/albums/1/photos"
}
方案二:把 WireMock 放进 Testcontainers
WireMockContainer 用独立容器提供模拟 HTTP 服务。JUnit 5 的 @Container 管理容器生命周期;这里使用原文镜像 wiremock/wiremock:2.35.0,通过 withMapping() 装载映射,通过 withFileFromResource() 装载响应文件。测试属性改为容器的 getBaseUrl()。
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.runtime.server.EmbeddedServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.wiremock.integrations.testcontainers.WireMockContainer;
import java.util.Collections;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
@Testcontainers(disabledWithoutDocker = true)
class AlbumControllerTestcontainersTests {
@Container
static WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:2.35.0")
.withMapping("photos-by-album", AlbumControllerTestcontainersTests.class, "mocks-config.json")
.withFileFromResource(
"album-photos-response.json",
AlbumControllerTestcontainersTests.class,
"album-photos-response.json");
@NonNull
public Map<String, Object> getProperties() {
return Collections.singletonMap("micronaut.http.services.photosapi.url",
wiremockServer.getBaseUrl());
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
}
@Test
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
Long albumId = 2L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
}
@Test
void shouldReturnEmptyPhotos() {
Long albumId = 3L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", nullValue());
}
}
}
容器资源以测试类的包路径放置:src/test/resources/example/micronaut/AlbumControllerTestcontainersTests/mocks-config.json,以及同目录的 album-photos-response.json。后者使用前面相同的两个照片对象。原文容器映射如下:
{
"mappings": [
{
"request": {
"method": "GET",
"urlPattern": "/albums/([0-9]+)/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFileName": "album-photos-response.json"
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/2/photos"
},
"response": {
"status": 500,
"headers": {
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"urlPattern": "/albums/3/photos"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": []
}
}
]
}
内嵌依赖 3.0.4 与容器镜像 2.35.0 不是同一个 WireMock 版本。本文保留原文版本,并不据此保证跨版本行为完全一致;更新镜像或客户端时,应单独核对兼容性。
空结果断言的实际差异:原文和下载包的相册3映射都是 jsonBody: [],但 shouldReturnEmptyPhotos() 使用 .body(“photos”, nullValue())。空数组、显式 null 和字段缺失是不同契约。下载包 application.properties 只配置了上游地址,没有在其中解释空数组为何应变为 null。我们未运行对应序列化路径,所以不声称这个断言已经通过,也不把它悄悄改写成同一含义。
如果你的接口约定 photos 始终为数组,编辑建议把这条断言改成 hasSize(0),并用该项目实际依赖版本验证响应确为 [];如果接口设计允许省略空字段,则应明确该序列化规则,并写能区分缺失、null 与 [] 的断言。下面仅是“空数组契约”的修订片段,不代表已验证的原文输出。
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(0));
方案三:用 MockServer 设置期望并核验交互
MockServer 可以为 HTTP/HTTPS 请求设置响应,也可作为记录或修改请求响应的代理,甚至对部分请求做代理、其他请求做模拟。本文仅使用其响应模拟和请求次数验证功能。加入 Testcontainers MockServer 模块与 Java 客户端:
testImplementation("org.testcontainers:testcontainers-mockserver")
testImplementation("org.mock-server:mockserver-client-java:5.15.0")
下载包的 MockServer Java 客户端与镜像均为 5.15.0。Testcontainers 依赖的具体管理版本由项目依赖管理决定,本文未进行解析。下面的 AlbumControllerMockServerTest.java 使用 @MicronautTest 启动上下文和嵌入式服务器;TestPropertyProvider 在上下文建立前提供模拟服务地址。实现该接口的 JUnit 测试采用 PER_CLASS 生命周期。
package example.micronaut;
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.micronaut.core.annotation.NonNull;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.test.support.TestPropertyProvider;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.Header;
import org.mockserver.verify.VerificationTimes;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.Collections;
import java.util.Map;
@MicronautTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Testcontainers(disabledWithoutDocker = true)
class AlbumControllerMockServerTest implements TestPropertyProvider {
@Container
static MockServerContainer mockServerContainer = new MockServerContainer(
DockerImageName.parse("mockserver/mockserver:5.15.0")
);
static MockServerClient mockServerClient;
@Override
public @NonNull Map<String, String> getProperties() {
mockServerContainer.start();
mockServerClient =
new MockServerClient(
mockServerContainer.getHost(),
mockServerContainer.getServerPort()
);
return Collections.singletonMap("micronaut.http.services.photosapi.url",
mockServerContainer.getEndpoint());
}
@BeforeEach
void setUp() {
mockServerClient.reset();
}
@Test
void shouldGetAlbumById(RequestSpecification spec) {
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"
}
]
"""
)
)
);
spec
.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);
}
private void verifyMockServerRequest(String method, String path, int times) {
mockServerClient.verify(
request().withMethod(method).withPath(path),
VerificationTimes.exactly(times)
);
}
}
getProperties() 中先启动容器,再用宿主地址与映射端口建立 MockServerClient,并把 photosapi.url 设置为 getEndpoint()。@BeforeEach 的 reset() 清除上一用例的期望与记录,防止不同用例互相影响。用例为 GET /albums/1/photos 返回两张照片的 JSON,再验证应用响应。
RequestSpecification 由 Micronaut Test REST-Assured 注入,已经带有嵌入式服务器端口,因此这里不必像前面的手工服务器用例那样设置 RestAssured.port。最后 verifyMockServerRequest() 使用 VerificationTimes.exactly(1),核查上游指定路径恰好被调用一次。返回内容和外部交互次数是两类不同断言,都有价值。
原页 MockServer 分支只展示成功响应和一次调用核验;WireMock 分支另外展示500与空结果。不能把前一分支的覆盖范围自动算到后一分支上,也不能据这个单次调用断言证明整个应用没有其他外部网络访问。
执行测试时要读报告,而不是只看结束状态
官方给出的运行命令是 ./gradlew test,测试报告位于 build/reports/tests/test/index.html。Windows 可使用项目随附的 gradlew.bat test。本文只读取源码与资源,没有执行 Gradle、Docker、容器镜像或任何测试,因此没有本轮通过率、运行时长和真实上游可用性结论。
./gradlew test
真正执行时,应确认容器确实启动、应用使用的是模拟地址、期望命中、200/500/空结果断言符合项目契约,以及 disabledWithoutDocker 没有让预期测试全被跳过。固定响应能让测试可重复,但也会与真实服务一起变旧;它证明应用如何处理这些合成响应,不证明 JSONPlaceholder 当前在线或接口永远不变。
零测试发现也可能显示成功:随文源码的 build.gradle 把 failOnNoDiscoveredTests 设为 false。因此仅凭 ./gradlew test 任务成功不足以确认测试被发现或执行。应打开报告核实发现、通过、失败、跳过的数量;用于持续集成时,按项目需要恢复“未发现测试即失败”的保护并确认过滤器没有排除预期测试。
许可与后续阅读
代码版权保留为 Copyright 2017–2026 original authors,按 Apache License 2.0 提供;原指南的文字和媒体按 CC BY 4.0 提供。本译稿保留 Sergio del Amo 署名、原始链接和改动说明:合并三个方案、补齐响应文件、以下载包纠正JDK前提、标出空数组断言差异,并给出限定范围的编辑修订。示意图由本文绘制,非运行截图。本中文译稿保留来源与作者署名,注明合并与修改;配图为编辑绘制。
继续阅读:WireMock 文档、Testcontainers WireMock module、MockServer。本篇静态代码审核未发现实际硬编码秘密;示例外部地址和照片字段均是公开测试数据。依赖、旧镜像、错误契约与测试跳过条件仍需在目标项目中实际验证,不作无漏洞或测试通过保证。
Apache License 2.0
The translated code excerpts in this article are provided under Apache License 2.0. The complete license text follows.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.











暂无评论内容