本文译自官网,做了格式修订。
MockServer是一个开源项目,其中有所有使用方式的示例。
阅读本文前,需要先理解什么是MockServer,请查看MockServer的基本介绍:MockServer简介
1.使用 MockServer 的步骤
- 启动 MockServer
- 设置期望
- 运行测试
- 验证请求
有关示例代码,请参阅 git 存储库中的 code examples folder 。
2. 启动 MockServer
MockServer 非常灵活,支持多种使用模式。
2.1 MockServer 支持以如下方式运行
- 使用
@Before或@After注解方法; - 在JUnit4中,使用
@Rule注解 - 在JUnit5中,使用
@ExtendWith注解 - 通过
@MockServerTest注解测试类,使用 Spring Test Execution Listener - 在支持Docker的环境中,以 Docker 容器来运行
- 在任何 Kubernetes 环境中,通过 Helm 图表运行
- 作为测试环境中的独立进程,从命令行运行
- 通过 Maven 插件作为 Maven 构建周期的一部分
- 作为任何Node.js代码的Node.js (npm) 模块
- 通过 Grunt 插件,作为 Grunt 构建周期的一部分
- 可以作为一个WAR包,部署到现有的应用服务器上
为了简化配置,所有版本(可部署的 WAR包 除外)都使用单个端口来支持 HTTP、HTTPS 或 SOCKS 中的控制平面和数据平面。
2.2 MockServer 的提供形式
- Java 依赖关系
- Docker 容器
- Kubernetes 的 Helm 图表
- 可执行 jar包
- Homebrew 软件包
- Maven 插件
- npm 插件
- Grunt 插件
- 可部署的 WAR包
也可以直接从源代码构建和运行 MockServer
2.3 MockServer 的界面
MockServer 有一个 UI,可用于查看 MockServer 中的内部状态:
- 日志
- 活跃的期望
- 收到的请求
- 代理的请求
3. MockServer的依赖项
MockServer 是一个独立的 HTTP mock 服务工具,Spring Boot 默认并未内置其支持,所以引入 spring-boot-test-starter之后,仍然需要单独引入MockServer等依赖项。
MockServer 提供了多个模块和依赖项,分别用于不同的使用场景:
3.1 核心依赖项mockserver-netty
mockserver-netty 是MockServer 的核心模块,包含运行 MockServer 所需的所有功能,用于嵌入式或独立运行的 MockServer 实现。当需要运行一个完整的 MockServer 时(如在本地测试或集成测试中)使用。
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.15.0</version>
</dependency>
3.2 客户端依赖项mockserver-client-java
mockserver-client-java是MockServer 提供的 Java 客户端库,用于与 MockServer 交互。作用是通过 Java 代码配置 MockServer 的行为或验证请求。使用场景是当 MockServer 作为服务运行时,需要使用客户端与其交互。
这个依赖项专注于与 MockServer 通信的客户端库,主要用于配置和验证,不包含 MockServer 的运行逻辑。
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java</artifactId>
<version>5.15.0</version>
</dependency>
3.3 精简版本mockserver-client-java-no-dependencies
这是MockServer 客户端的精简版本,不包含其依赖项。作用是提供更轻量的依赖,适合对依赖管理有严格要求的项目(如避免重复依赖冲突)。当项目中已经引入了 Apache HttpClient 或其他 MockServer 的依赖,可以使用这个版本以减少重复依赖。
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java-no-dependencies</artifactId>
<version>5.15.0</version>
</dependency>
3.4. 依赖项的选择
-
需要运行 MockServer:使用
mockserver-netty。 -
只需配置或验证 MockServer:使用
mockserver-client-java。 -
轻量化需求:使用
mockserver-client-java-no-dependencies,并手动管理其他依赖项。
4. 设置期望(Expectation)
4.1 Java代码示例
new MockServerClient("localhost", 1080)
.when(
request()
.withMethod("POST")
.withPath("/login")
.withBody("{username: 'foo', password: 'bar'}")
)
.respond(
response()
.withStatusCode(302)
.withCookie(
"sessionId", "2By8LOhBmaW5nZXJwcmludCIlMDAzMW"
)
.withHeader(
"Location", "https://www.mock-server.com"
)
);
在 Java 中使用时,要添加依赖项,例如在 Maven 中:
<dependency
<groupIdorg.mock-server</groupId>
<artifactId>mockserver-client-java-no-dependencies</artifactId>
<version>RELEASE</version>
</dependency>
4.2. 匹配期望的请求可能包括:
- Request matcher - 请求匹配器,用于匹配此期望的请求
- action,- 执行的操作,操作包括 response、forward、callback 和 error
- times(可选) - 执行操作的次数
- timeToLive(可选) - 期望保持活动状态的时间
- priority(可选) - 请求匹配会先按优先级(最高优先)排序,然后按创建时间(最早优先)排序
- id(可选) - 用于更新现有期望(即当 id 匹配时)
支持使用 OpenAPI v3 规范来为每个操作生成请求匹配器期望。
4.3 匹配顺序
MockServer 将按照添加“活跃期望”的时间顺序,来匹配“活跃期望”(如果它们的优先级相同)。
例如,如果在一个请求匹配器中,添加期望 A时使用 Times.exactly(3) ,添加期望 B时使用 Times.exactly(2),则它们将按以下顺序来应用:A、A、A、B、B。
匹配时,先按优先级排序(最高优先),再按创建顺序排序(最早优先)。
默认期望或响应的优先级,可以指定为一个负值,并配合以非常宽松的请求匹配器。松散的请求匹配器以确保默认期望始终能够匹配,低优先级确保它在所有其他期望之后最后匹配。
4.4. 更新期望
添加一个期望时,如果其 id 字段与现有某个期望的 id 一致,则新的期望将替换掉现有的期望(即实现了期望的更新)。如果未指定 id 值,则将使用分配给每个期望的 UUID。
5. 请求匹配器(request matcher)
有两种类型的请求匹配器:
- 属性请求匹配器 - 使用 HTTP 属性(如方法、路径或正文)匹配请求
- open api 请求匹配器 - 使用 OpenAPI(Swagger3.0)的规范定义匹配请求
5.1. 属性请求匹配器
5.1.1 请求匹配器使用的属性
请求属性匹配器使用以下一个或多个属性匹配请求:
- method - [property matcher]属性匹配
- path - [property matcher] 属性匹配
- path parameters - [key to multiple values matcher] 多值匹配的键
- query string parameters - [key to multiple values matcher] 多值匹配的键
- headers - [key to multiple values matcher] 多值匹配的键
- cookies - [key to single value matcher] 单值匹配的键
- body - [body matchers] 请求体匹配
- secure 安全布尔值,如果是https则为true
5.1.2 属性请求匹配器完成匹配的方法
可以使用以下方法完成属性匹配:
- string value 字符串值
- 用于:方法、路径、路径参数键、路径参数值、查询参数键、查询参数值、标头键、标头值、Cookie 键、Cookie 值或正文
- regex value 正则表达式值
- 用于:方法、路径、路径参数键、路径参数值、查询参数键、查询参数值、标头键、标头值、Cookie 键、Cookie 值或正文
- json schema JSON 架构
- 用于:方法、路径、路径参数值、查询参数值、标头值、Cookie 值或正文
- 不支持:路径参数键、查询参数键、标头键或 Cookie 键
- optional value 可选值
- 用于:方法、路径、路径参数值、查询参数键、查询参数值、标头键、标头值、Cookie 键、Cookie 值或正文
- 不支持以下项:路径参数键或值、查询参数值、标头值或 Cookie 值
- negated value 否定值
- 用于:方法、路径、路径参数键、路径参数值、查询参数键、查询参数值、标头键、标头值、Cookie 键、Cookie 值或正文
5.1.3 键与多值的匹配方式
键与多值的匹配方式,支持标头、查询参数和路径参数的每个键都匹配多个值
- 键 支持除 json 架构之外的所有属性匹配器
- 值 支持除“optional values”之外的所有属性匹配器
- 匹配支持两种模式:
- sub set(默认)子集模式,如果 request 属性包含匹配的子集(考虑可选键),则匹配,因此每个非可选键或可选键(如果存在)至少有一个匹配值
- [matching key]匹配键模式,- 如果请求属性仅包含匹配值(考虑可选键),则匹配,因此每个非可选键或可选键(如果存在)的所有值都必须匹配
5.1.4 键与单值的匹配方式
将键与单值匹配,支持对 cookie 的每个键使用不同的单个值
- 键 支持除 json 架构之外的所有属性匹配器
- 值 支持除“optional values”之外的所有属性匹配器
5.1.5 Body的匹配方式
可以使用以下方法完成Body的匹配:
- 纯文本(即完全匹配)
- 正则表达式 - 参见 Java 正则表达式语法
- JSON:支持
- matchType:来控制匹配哪些字段:
- STRICT:匹配所有字段、数组顺序,不允许其他字段
- ONLY_MATCHING_FIELDS:仅匹配请求匹配器中提供的字段
- JsonUnit:占位符,以允许忽略字段或值或按类型匹配,例如:
- ${json-unit.ignore-element}:忽略字段
- ${json-unit.any-boolean}: 将字段匹配为任何布尔值
- matchType:来控制匹配哪些字段:
- Json Schema
- JsonPath:如果表达式返回至少一个值,则匹配
- XML:
- XMLUnit 占位符,用于允许忽略字段或元素或按类型匹配,例如:
- ${xmlunit.ignore} 忽略元素
- ${xmlunit.isNumber} 将元素或属性匹配为数字
- XMLUnit 占位符,用于允许忽略字段或元素或按类型匹配,例如:
- XML Schema: - 请参阅 XML 模式文档
- XPath - 如果表达式返回的至少一个值匹配,请参阅XPath规范
- form fields:表单字段(即正文参数)
- binary:二进制匹配
- negated matcher:否定匹配器
5.1.6 属性请求属性匹配的代码示例
更多示例,请参阅 git 存储库中的 Code Examples 文件夹: code examples folder
为简洁起见,Java 代码示例中未包含静态导入,因此在复制此代码时,请添加以下静态导入
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
具体每个匹配的代码请参考原文,以下仅列出第一个作为示例:
- match requests using priority:使用优先级的请求匹配
new MockServerClient("localhost", 1080)
.when(
request()
.withPath("/some/path"),
Times.once(),
TimeToLive.exactly(TimeUnit.SECONDS, 60L),
10
)
.respond(
response()
.withBody("some_response_body")
);
| match requests using default expectation | 使用默认期望匹配请求 |
|---|---|
| match request by path | 按路径匹配请求 |
| match request by path exactly twice | 按路径精确匹配请求两次 |
| match request by path exactly once in the next 60 seconds | 在接下来的60秒内按路径匹配一次请求 |
| match request by regex path | 通过正则表达式路径匹配请求 |
| match request by not | 通过not匹配请求 |
| matching path | 匹配路径 |
| match request by path parameter and query parameter | —根据路径参数和查询参数匹配请求 |
| match request by path parameter regex value | 根据路径参数的regex值匹配请求 |
| match request by path parameter with json schema value | 根据路径参数匹配请求与json模式值 |
| match request by method regex | 通过regex方法匹配请求 |
| match request by not matching method | 通过不匹配方式匹配请求 |
| match request by query parameter with name regex | 匹配名称为regex的查询参数 |
| match request by query parameter with regex value | 将查询参数与regex值匹配 |
| match request by query parameter with json schema value | 根据查询参数与json模式值匹配请求 |
| match request by optional query parameter | 根据可选查询参数匹配请求 |
| match request by query parameter as sub set | 按查询参数匹配请求为子集 |
| match request by query parameter as matching key | 将查询参数作为匹配键匹配请求 |
| match request by headers | 根据请求头匹配请求 |
| match request by header name regex | 通过头部名称正则表达式匹配请求 |
| match request by header name and regex value | 根据首部名称和正则表达式值匹配请求 |
| match request by not matching header value | 通过不匹配首部值来匹配请求 |
| match request by not matching headers | 通过不匹配请求头来匹配请求 |
| match request by header with json schema value | 根据请求头匹配json schema值 |
| match request by either one header or another header using optional header | 使用可选的header匹配请求的任何一个header或另一个header |
| match request by header as matching key | 将请求头作为匹配键进行匹配 |
| match request by cookie and query parameter | 根据cookie和查询参数匹配请求 |
| match request by cookie and query parameter with json schema values | 根据cookie和查询参数与json模式值匹配请求 |
| match request by optional cookie | —可选匹配cookie请求 |
| match request by body sub-string | 根据body子字符串匹配请求 |
| match request by body in utf16 | 根据utf16的body匹配请求 |
| match request by regex body | 通过正则表达式体匹配请求 |
| match request by form submission body | 根据表单提交体匹配请求 |
| match request by body with xpath | 使用xpath通过body匹配请求 |
| match request by not matching body with xpath | 通过不使用xpath匹配请求体来匹配请求 |
| match request by body with xml | 通过XML正文匹配请求 |
| match request by body with xml using placeholders | 使用占位符将请求正文与XML匹配 |
| match request by body with xml schema | 通过XML schema匹配请求主体 |
| match request by body with xml schema by classpath | 通过类路径将请求体与XML模式进行匹配 |
| match request by body with json exactly | 将请求体与json精确匹配 |
| match request by body with json ignoring extra fields | 通过body匹配请求,json忽略额外的字段 |
| match request by body with json ignoring extra fields in array objects | 使用json匹配请求体,忽略数组对象中的额外字段 |
| match request by body with json using placeholders | 使用占位符将请求正文与json匹配 |
| match request by body with json schema | 使用json模式通过body匹配请求 |
| match request by body with jsonpath | 将请求体与jsonpath匹配 |
| match request by not matching body with JsonPath | 通过不将请求体与JsonPath匹配来匹配请求 |
| match request with binary PNG body | 使用二进制PNG正文匹配请求 |
| update expectation by id | 通过id更新期望 |
5.2 Open Api 请求匹配器
5.2.1 Open API 请求匹配器的字段
- specUrlOrPayload - 必需值,符合OpenAPI v3 规范( JSON 或 YAML 格式),可以是:
- HTTP/HTTPS URL
- 文件 URL
- 类路径位置(不带类路径:scheme)
- 内联 JSON 对象]
- 内联转义 YAML 字符串
- operationId: 可选值,用于指定要匹配的操作,如果为空或 null,则匹配所有操作
MockServer 为每个Open API 请求匹配器创建一组请求属性匹配器,以确保控制面板逻辑(如清除期望或检索期望)在两种类型的请求匹配器之间一致地工作,这可以在 MockServer UI 活动期望部分查看。
5.2.2 OpenAPI请求匹配器的示例代码
具体每个匹配的代码请参考原文,以下仅列出第一个作为示例:
- 通过HTTP Url 加载 OpenAPI 匹配请求
new MockServerClient("localhost", 1080)
.when(
OpenAPI(
"https://raw.githubusercontent.com/mock-server/mockserver/master/mockserver-integration-testing/src/main/resources/org/mockserver/OpenAPI/OpenAPI_petstore_example.json"
)
)
.respond(
response()
.withBody("some_response_body")
);
- 通过OpenAPI操作匹配请求
- 根据文件url加载 OpenAPI匹配请求
- 根据classpath位置加载 OpenAPI匹配请求
- 通过json字符串文字量加载 OpenAPI匹配请求
6. 操作
6.1 操作的类型
操作可以是以下类型之一:
- response:返回使用以下方式定义的一个响应
- 文字
- JavaScript 模板
- velocity模板
- 类回调(必须存在于类路径中)
- 方法/闭包回调(通过 WebSocket)
- forward:使用以下方式时,转发修改后的请求并返回修改后的响应
- 收到的确切请求和响应
- 静态覆盖的请求和/或响应
- 动态修改的请求和/或响应
- JavaScript 模板(仅限请求)
- velocity模板(仅限请求)
- 请求和/或响应的类回调(必须存在于类路径中)
- 请求和/或响应的方法/闭包回调(通过 WebSocket)
- error: 以字节序列的形式返回无效响应或关闭连接
如果一个请求因为没有任何一个匹配器能够与其匹配,而导致没有操作,则:
- 如果 MockServer 被用作请求代理,则MockServer将不对这个请求做任何修改,而直接将其发送给目标地址
- 如果请求的主机标头和主机名称或IP不匹配,则请求会自动代理到主机标头中的地址
6.2 响应操作
响应操作可以是:
- 包含以下任一内容的响应文本:
- status code 状态码
- reason phrase 原因的描述
- body :响应体
- headers :表头
- cookies
- delay :延迟
- 可用于禁止显示标头、覆盖标头或关闭套接字连接的选项
- 使用 JavaScript 或 Velocity 的模板化响应,但有延迟
- 或用于根据请求动态生成响应的回调:
- 作为服务器端回调,实现为具有默认构造函数的 Java 类,实现 org.mockserver.mock.action.ExpectationResponseCallback 并在类路径上可用
- 作为客户端回调,使用 Java 或 JavaScript 客户端实现为闭包
6.3 响应操作示例
下以下仅列出第一个作为示例,具体每个响应的代码请参考原文:
- 只有body的文字响应
new MockServerClient("localhost", 1080)
// this request matcher matches every request
.when(
request()
)
.respond(
response()
.withBody("some_response_body")
);
- UTF16正文的文字响应
- body为UTF8的json响应
- 带有header的Json响应
- 使用cookie的Json响应
- 文字响应与状态码和原因描述
- 文字响应二进制PNG正文
- 10秒延迟的文字响应
- 延迟相同请求的响应不同
- 文字响应,带有连接选项,以抑制标头
- 文字响应,带有连接选项以覆盖标头
- 文字响应,带有关闭套接字的连接选项
- 文字响应,带有连接选项,在延迟后关闭套接字
- Javascript模板化响应
- Javascript模板化响应读取请求体
- Javascript使用延迟模板化响应
- 速度模板响应
- 类的回调
- 方法/闭包回调
- 在方法/闭包回调中创建期望
6.4. 请求转发的操作
请求转发的操作可以是:
- 精确的转发器,它完全按照接收请求的方式转发请求,其中包含以下内容:
- host
- port
- scheme
- 被覆盖的请求(或被覆盖的响应),但有延迟,允许替换转发的请求或响应的任何部分,或修改某些字段(路径、标头、cookie 或查询参数)
- 使用 JavaScript 或 Velocity 的模板化转发器,具有延迟,允许在转发请求之前对其进行修改或完全重写
- 根据 MockServer 收到的请求,动态生成转发请求的回调:
- 作为服务器端回调实现,该类具有默认构造函数,实现 org.mockserver.mock.action.ExpectationForwardCallback 或 org.mockserver.mock.action.ExpectationForwardAndResponseCallback,并且在类路径上可用
- 作为客户端回调,使用 Java 或 JavaScript 客户端实现为闭包
6.5 转发请求的代码示例
下面的代码示例演示如何创建不同的转发操作,以下仅列出第一个示例,具体代码请参考原文:
- 转发精确请求
new MockServerClient("localhost", 1080)
.when(
request()
.withPath("/some/path")
)
.forward(
forward()
.withHost("mock-server.com")
.withPort(80)
);
- 通过HTTPS转发准确的请求
- 转发覆盖请求
- 转发覆盖的请求和响应
- 转发被覆盖和修改的请求
- 转发覆盖和修改的请求和响应
- 转发覆盖的请求并更改主机和端口
- 延迟转发覆盖请求
- Javascript模板转发
- Javascript模板前剥离路径前缀
- Javascript模板化延迟转发
- 速度模板前进
- 类回调来覆盖请求和响应
- 方法/闭包回调来覆盖请求
6.6 错误操作或示例代码
错误操作可能会以字节序列的形式返回无效响应或断开连接,下面的代码示例演示如何创建不同的错误操作。
- random bytes error: 随机字节错误
// generate random bytes
byte[] randomByteArray = new byte[25];
new Random().nextBytes(randomByteArray);
new MockServerClient("localhost", 1080)
.when(
request()
.withPath("/some/path")
)
.error(
error()
.withDropConnection(true)
.withResponseBytes(randomByteArray)
);
- drop connection error:连接丢失错误
new MockServerClient("localhost", 1080)
.when(
request()
.withPath("/some/path")
)
.error(
error()
.withDropConnection(true)
);
7. 验证请求
MockServer 支持验证已收到的请求,包括代理请求和符合预期的请求。
可以按如下方式指定验证:
- 请求匹配器,标识应匹配请求的次数的条件
- 按顺序匹配的请求匹配器序列
7.1 验证重复请求
使用 MockServer 验证是否已收到特定次数的请求。
验证重复请求的示例代码(请查看原文获得所有的完整示例 ):
- 验证收到的请求至少两次
new MockServerClient("localhost", 1080)
.verify(
request()
.withPath("/some/path"),
VerificationTimes.atLeast(2)
);
- 最多验证两次收到的请求:
VerificationTimes.atMost(2) - 验证收到两次请求:
VerificationTimes.exactly(2) - 验证OpenAPI收到的请求至少两次:
- 验证OpenAPI和operation一次收到的请求:
- 根据收到一次期望id的请求:
- 验证从未收到的请求:
VerificationTimes.exactly(0)
7.2. 验证请求序列
使用 VerificationSequence,可以验证 MockServer 是否已按指定顺序接收到一系列请求。序列中的每个请求将按照指定的确切顺序验证是否至少已收到一次。
验证请求顺序的示例代码:
- 验证收到的请求序列
new MockServerClient("localhost", 1080)
.verify(
request()
.withPath("/some/path/one"),
request()
.withPath("/some/path/two"),
request()
.withPath("/some/path/three")
);
- 使用OpenAPI验证接收到的请求序列:
- 使用期望id验证收到的请求序列: