MockServer的使用

1,362 阅读17分钟

本文译自官网,做了格式修订。

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}: 将字段匹配为任何布尔值
  • Json Schema
  • JsonPath:如果表达式返回至少一个值,则匹配
  • XML:
    • XMLUnit 占位符,用于允许忽略字段或元素或按类型匹配,例如:
      • ${xmlunit.ignore} 忽略元素
      • ${xmlunit.isNumber} 将元素或属性匹配为数字
  • 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 响应操作

响应操作可以是:

  • 包含以下任一内容的响应文本:
  • 可用于禁止显示标头、覆盖标头或关闭套接字连接的选项
  • 使用 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验证收到的请求序列:

参考

  1. MoccServer简介
  2. MockServer的使用
  3. Mockto应用指南
  4. 从类结构理解MockMVC
  5. Junit5单元测试