quarkus-wiremock
最近项目中接触到wiremock的测试,之前一直是写Mockito,也就是最传统的对 本地代码(类、接口、方法)进行行为模拟。
比如我现在有一个TaobaoService接口,我可以对其方法进行模拟,当请求特定或任意资源的时候,返回我们想要返回的东西。1
2
3
4
5
6
7
8
9
10
11
12
13
14
lateinit var taobaoService: TaobaoService
fun setUp() {
val taobaomockResponse = Fixture.loadPOJOFromFile<TaobaoTradeApi>("/examples/api_mock.json")
// 配置 mock 行为
`when`(taobaoService.taobaoTradeFullinfoGet(anyString(), anyString(), anyString()))
.thenReturn(taobaomockResponse)
`when`(taobaoService.taobaoTradeFullinfoGet(anyString(), anyString()))
.thenReturn(taobaomockResponse)
}
但是我们遇到了一种情况,此时无法使用Mockito:
在 MVC 项目里,我们想在 Model 层直接注入一个 TaobaoService 来方便调用,但 Model 并不在应用作用域(appScope)内,导致依赖注入失效。于是我们只能自己 new
一个 TaobaoService。问题是,这个手动创建的实例在单元测试里无法被 Mockito 的 @InjectMock
替换,Mock 失效,测试就会去请求真实的 Taobao API。
此时就可以用到WireMock来实现我们想要的功能。WireMock 是针对 HTTP 服务 的模拟服务器。
它启动一个内嵌的 HTTP 服务器,配置请求─响应映射(stub),拦截真正的 HTTP 调用并返回预先定义的 mock 数据。
使用场景
框架 | 主要用途 |
---|---|
Mockito | 单元测试中替代依赖对象:DAO、Service、工具类等。 |
WireMock | 集成测试或端到端测试中,替代外部 HTTP 服务(第三方 API、微服务) |
优劣对比
维度 | Mockito 优势 | Mockito 劣势 | WireMock 优势 | WireMock 劣势 |
---|---|---|---|---|
依赖范围 | - 直接在 JVM 内部拦截、速度快 - 无需网络,启动轻量 |
- 只针对本地方法,无法模拟 HTTP 底层细节 | - 真正启动 HTTP 服务,完全模拟外部 HTTP 接口 - 支持延迟、故障注入等 |
- 启动一个 HTTP Server,整体 启动&执行 较慢 - 需占用端口 |
配置方式 | - 代码级别声明 - 灵活控制单个方法的调用和返回值 |
- 无法对 URL、Header、Body 级别进行精细匹配 | - 支持 URL、Query、Header、Body 等多维度匹配 - Stub 配置直观 |
- Stub 配置较多时文件/代码会较冗长 |
断言能力 | - 可以直接对方法调用次数、入参、顺序进行精确断言 | - 仅限 Java 对象,不支持对 HTTP 原始报文断言 | - 可以对 HTTP 请求日志做断言(请求次数、Body 内容等) | - 断言次数或内容变化时,需要额外调用 admin API |
故障模拟 | - 可抛出任意异常模拟下游组件故障 | - 无法模拟网络超时、延迟等真实 HTTP 场景 | - 原生支持延迟、连接失败、按比例返回错误码等 | - 对非 HTTP 场景无能为力 |
集成成本 | - 几行代码即可完成 Mock - 与测试框架无缝集成 |
- 仅组件级 Mock,不具备对外部调用的覆盖 | - 可用独立进程或内嵌;支持 Docker 化 | - 需要管理服务器生命周期;CI 环境中要配置端口和依赖 |
引入依赖
官方文档
官方文档中是Maven导入,我们用Gradle的话,可以这样写:1
testImplementation("io.quarkiverse.wiremock:quarkus-wiremock-test:1.0.0")
也可以使用libs.versions.toml
来配置版本:1
2
3
4[versions]
wiremock-test = "1.0.0"
[libraries]
wiremock = {group = "io.quarkiverse.wiremock", name = "quarkus-wiremock-test", version.ref = "wiremock-test" }
然后再build.gradle.kts
中写:1
testImplementation(libs.wiremock)
implementation & testImplementation
这里WireMock
是用implementation
还是testImplementation
呢?
implementation(“…”)
- 作用范围:对项目的 主代码(
src/main/java
/src/main/kotlin
)可见,也会出现在运行时和最终打包的产物里。 - 典型用途:那些在生产环境里真正需要、无论是编译还是运行都离不开的库,比如 Quarkus 运行时、数据库驱动、核心工具库等。
- 作用范围:对项目的 主代码(
testImplementation(“…”)
- 作用范围:仅对 测试代码(
src/test/java
/src/test/kotlin
)可见,既会加入测试编译时的 classpath,也会加入测试执行时的 classpath;但不会出现在生产运行时,也不会被打包到发布产物里。 - 典型用途:只能在测试里用到的库,比如 JUnit、Mockito、WireMock、测试专用的工具集等。
- 作用范围:仅对 测试代码(
这里我们在开发完毕后需要将wiremock放到testImplementation
中,但是在开发的时候,为了方便调试,我们可以将其放到implementation
中,查看mappings是否正常运行。
配置WireMock
这是整个步骤中我觉得最困难的地方。由于我们是在测试中用到wiremock所以放在test下的application.properties
更好。
quarkus.devservices.enabled
这个全局开关默认为true,千万不能为诶false。否则wiremock就不会再测试或者本地开发模式下自动启动。
quarkus.wiremock.devservices.port
这个配置是定义wiremock启动后监听在哪个端口上,如果不配置,那么就随机制定。
通常我们不需要配置,但是如果要配置的话我们可以这样写:1
2%test.quarkus.wiremock.devservices.port=9081
%dev.quarkus.wiremock.devservices.port=9081
bug
这里注意了,当我们使用implementation
的时候,如果不配置test port,运行./gradlew quarkusDev
进行测试时会报错:
- 因为此时
implementation
把quarkus-wiremock-test
扩展放进 主应用的 classpath。 - 在 dev 模式下若没显式配置端口,启动时会直接尝试绑定 8089(官方默认)。
- 如果随后又运行测试(
r
快捷键),而测试也会再启一个 WireMock, 两份 WireMock 都想占 8089 → BindException。
在testImplementation
情况下 - WireMock 只出现在 test classpath,
quarkusDev
启动的主应用里根本“看不到”它,所以不会在开发模式就抢占端口。 - 只有在Continuous Testing(
r
)运行时,Quarkus 会为测试 fork/隔离一个专用的 测试 classloader,此时才加载 WireMock 扩展。 - 如果没配
quarkus.wiremock.devservices.port
,扩展会自动找 一个空闲端口,并把实际端口注入${quarkus.wiremock.devservices.port}
占位符
因此最好使用testImplementation
如果一定要在调试时使用implementation
的话,也请显示定义%test.quarkus.wiremock.devservices.port
quarkus.wiremock.devservices.files-mapping
先说现象
配置 | 连续测试 (./gradlew quarkusDev ➜ 键 r ) |
结果 |
---|---|---|
没有显式配置files-mapping |
Dev 模式能启动 WireMock,但 测试一跑就报 Un-recognized token ‘No’;/__admin/mappings 为空 |
失败 |
quarkus.wiremock.devservices.files-mapping=${pwd}/build/resources/test |
Dev 模式照常启动;按 r 运行测试也能找到所有 stub,错误消失 |
成功 |
./gradlew test
- Gradle 在运行
test
任务时,会把src/main/resources
src/test/resources
- 测试类与测试依赖
一起打包到 测试 JVM 的 classpath 上。
- WireMock Dev Service 默认会扫描 classpath 根下的
mappings/
和__files/
目录,
所以放在src/test/resources/mappings/…
的 stub 能被自动找到并注册,测试自然通过。
- Gradle 在运行
./gradlew quarkusDev
+ 按r
(Continuous Testing)- Quarkus Dev Mode 会先把 主应用(
src/main/java
+src/main/resources
)打包成 Runner Jar 并启动, - Continuous Testing 再在这个运行时环境里 热重载执行测试,但它并不把
src/test/resources
一并打到 Runner Jar 中。 - 因此,WireMock 在 Dev Mode 的 classpath 根下 看不到 测试资源里的
mappings/
——
它启动了空的 stub 存储,所有请求都返回 404 “Not Found”
- Quarkus Dev Mode 会先把 主应用(
测试代码
经过了漫长的配置,我们终于要开始mock http了,首先,我们需要对需要mock的RestClient对象上,不能设置BaseURI,而是要配置configKey,如下所示。
如下所示:1
2
3
4
5
6
7
8
9
10
11
12
xxx
interface TaobaoService {
fun taobaoTradeFullinfoGet(
String, session:
String, tid:
): TaobaoTradeApi
}
在 application.properties
里,我们给 Taobao 的客户端设置了两套地址:
- 测试环境(用
%test.
前缀)1
%test.quarkus.rest-client.taobao.url = http://localhost:${quarkus.wiremock.devservices.port}/router/rest/taobao
- ▶️ 当跑测试时,Quarkus 会自动把所有对
TaobaoService
的调用指向本地 WireMock 服务器,这样就能返回我们事先准备好的模拟数据。
- 开发/生产环境(用
%dev.
前缀)1
`%dev.quarkus.rest-client.taobao.url = https://eco.taobao.com/router/rest`
- ▶️ 在平时的开发或上线时,客户端会调用真正的淘宝 API。
Quarkus 会根据当前运行的 Profile(test
还是 dev
/prod
)自动选用对应的那一行配置,不需要手动切换。
在跑测试的时候,我们不想真打到淘宝 API,而是把所有 HTTP 请求都指向本地启动的 WireMock。
为了让 WireMock “知道” 遇到什么请求该怎么回,它需要一份“剧本”——这就是 stub。
stub 就像一套“如果看到这个请求,就给我返回那个响应”的规则:
1. **请求格式**:请求方法(GET/POST)、路径、Query 参数、Header、甚至 Body 长啥样
2. **返回结果**:要回哪个 HTTP 状态码,Body 是哪段 JSON,Header 应该带什么
我们把这些 stub 文件都放在 src/test/resources/mappings/
里,WireMock 启动时会自动读取它们。这样,测试一跑:
- 代码里发出的每个 HTTP 调用,都会被 WireMock 拦截;
- WireMock 根据 mappings 里的 stub 规则,马上返回我们预先写好的假数据;
- 测试就能在离线、可控的环境下稳稳地跑通,不会再去真正打淘宝接口。
比如我这里定义的taobao_v0-stubs.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72{
"id": "8a3782de-2eca-420b-9942-079e74919389",
"priority": 3,
"request": {
"method": "POST",
"urlPath": "/router/rest/taobao"
},
"response": {
"status": 200,
"jsonBody": {
"trade_fullinfo_get_response": {
"trade": {
"buyer_nick": "z**",
"buyer_open_uid": "AAEtbdvLAJmprSrbeTwWPXYC",
"created": "2024-12-25 17:02:02",
"modified": "2024-12-25 17:02:02",
"new_presell": false,
"orders": {
"order": [
{
"buyer_rate": false,
"cid": 50012587,
"num": 31,
"num_iid": 736399892063,
"oid": "3922360629974",
"outer_iid": "HWM60PQSNM",
"outer_sku_id": "1.01.08.2055",
"payment": "0.00",
"pic_path": "https://img.alicdn.com/bao/uploaded/i3/793375733/O1CN01hyrhao1sDlWBeKF7N_!!793375733.jpg",
"price": "24.87",
"refund_status": "NO_REFUND",
"sku_id": "5740608026382",
"sku_properties_name": "适用手机型号:华为 Mate 60 Pro;颜色分类:超清水凝膜2片【曲屏全覆盖*升级无气泡】送神器+防滑垫",
"status": "WAIT_BUYER_PAY",
"title": "【活动价】闪魔适用华为mate60pro钢化膜Mate60Pro手机膜mate 60高清60Pro+曲面RS非凡大师"
},
{
"buyer_rate": false,
"cid": 50023728,
"num": 32,
"num_iid": 727021460771,
"oid": "6880691104973",
"outer_iid": "9.09.02.0004",
"payment": "0.00",
"pic_path": "https://img.alicdn.com/bao/uploaded/i4/793375733/O1CN013d698Z1sDlV1Bk1Cp_!!0-item_pic.jpg",
"price": "9999.00",
"refund_status": "NO_REFUND",
"status": "WAIT_BUYER_PAY",
"title": "赠【30天贴坏包赔服务】-限时活动专属"
}
]
},
"payment": "19.90",
"receiver_city": "深圳市",
"receiver_country": "",
"receiver_district": "龙华区",
"receiver_state": "广东省",
"receiver_town": "观澜街道",
"seller_nick": "闪魔旗舰店",
"status": "WAIT_BUYER_PAY",
"tid": "12500568",
"total_fee": "10023.87",
"type": "fixed"
},
"request_id": "16l7c09wjvlxi"
}
},
"headers": {
"Content-Type": "application/json"
}
}
}
此stub包含如下信息
1. 基础元信息
id
:8a3782de-2eca-420b-9942-079e74919389
- 这是 WireMock 内部给这条映射规则的唯一标识,方便查日志或管理。
priority
:3
- 数值越小优先级越高,值相同则按照注册顺序。这里写
3
,表示中等优先级。
- 数值越小优先级越高,值相同则按照注册顺序。这里写
2. 请求匹配(request
)
1 | "request": { |
- HTTP 方法:
POST
需要和被mock的请求方法保持一致 - 路径:
/router/rest/taobao
需要和properties中配置的taobao.url
保持一致 - 没有对请求头、查询参数、甚至请求体做进一步校验——只要是 POST 到这个路径,都能命中。
3. 响应定义(
response
)
1 | "response": { |
- HTTP 状态:
200 OK
- 响应体:
- 这里直接把一个完整的 Java 对象格式的 JSON(即
trade_fullinfo_get_response
结构)内联在jsonBody
里。 - WireMock 会自动把它序列化成纯文本 JSON 响应。
- 这里直接把一个完整的 Java 对象格式的 JSON(即
- 响应头:
Content-Type: application/json
- 指明这是 JSON,方便客户端(你的 Quarkus Rest Client 或过滤器)正确解析。
4. 具体行为效果
- 客户端代码 发起
POST http://<wiremock-host>:<port>/router/rest/taobao
- WireMock 匹配到上面的 stub(方法和路径都符合)
- WireMock 立刻 返回
- 测试或业务代码拿到这个假数据,就不会再去真正调用淘宝 API,而是直接用这份“剧本”数据跑逻辑或断言。
进一步拆分
事实上,我们可以把大块的响应 JSON 放到__files/
目录下,然后在mappings/
里只用一个很短的映射文件指向它。目录结构大致是:1
2
3
4
5
6
7src
└─ test
└─ resources
├─ mappings
│ └─ taobao_v0-stubs.json
└─ __files
└─ taobao-response.json
mappings/taobao_v0-stubs.json如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
"id": "8a3782de-2eca-420b-9942-079e74919389",
"priority": 3,
"request": {
"method": "POST",
"urlPath": "/router/rest/taobao"
},
"response": {
"status": 200,
"bodyFileName": "taobao-response.json",
"headers": {
"Content-Type": "application/json"
}
}
}
taobao-response即我们要返回的jsonbody