WireMock在匹配URL查询参数时,默认拿到的参数值已经是解码后的字符串。只要这个字符串本身是合法XML,就可以用matchesXPath按节点和文本断言,而不是把整段XML当成普通文本做包含匹配。这种方式特别适合测试那些通过GET请求传递搜索条件、业务单据或配置片段的接口。

WireMock为什么能用XPath匹配查询参数中的XML
一个典型请求可能长这样:路径是/search,查询参数名叫payload,参数值是一段XML文本。浏览器或HTTP客户端在发送前通常会把XML里的特殊字符做百分号编码,例如尖括号、斜杠、与符号等。WireMock收到请求后,会先按标准查询字符串规则解析参数,把payload对应的值还原成原始文本,再交给匹配器判断。
这意味着,如果还原后的文本是合法XML,matchesXPath就可以把它当作XML文档来求值。你可以只关心某个节点是否存在,也可以只关心某个节点文本是否等于指定值。相比contains或equalTo,XPath的优势在于它表达的是结构语义,而不是字符序列。即使XML里多了空格、换行、属性顺序变化,只要目标节点和文本值没变,匹配仍然可以通过。
当然,这种能力也有边界。WireMock不会替你把一段非XML文本强行解释成XML。如果参数值只是普通字符串、JSON文本,或者XML语法本身不完整,XPath求值就会失败。实际使用时,建议先确认业务确实把XML放在查询参数中,而不是放在请求体里;如果XML在请求体中,应该使用请求体匹配器,而不是查询参数匹配器。
基础示例:匹配order/id文本值
假设被测接口通过payload参数接收如下XML,逻辑结构是订单节点下有一个编号节点。真实请求中,这段XML会被编码成一个查询参数值,而不是以明文尖括号直接出现在URL里。
GET /search?payload=%3Corder%3E%3Cid%3E123%3C%2Fid%3E%3C%2Forder%3E HTTP/1.1 Host: localhost:8080
如果只想命中编号为123的请求,可以使用JSON映射文件配置stub。下面示例中,urlPath只负责匹配路径,查询参数条件单独写在queryParameters里,这样结构更清晰,也避免把路径和参数混在一个字符串里造成转义困难。
{
"request": {
"method": "GET",
"urlPath": "/search",
"queryParameters": {
"payload": {
"matchesXPath": "/order/id[text()='123']"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "text/plain; charset=utf-8"
},
"body": "matched"
}
}如果你更习惯使用Java DSL,也可以写出等价规则。Java DSL的优势是方便在单元测试里动态构造桩,并且可以结合常量、变量和参数化测试使用。下面代码假设已经静态导入了WireMock的常用匹配器和构造器。
import static com.github.tomakehurst.wiremock.client.WireMock.*;
stubFor(get(urlPathEqualTo("/search"))
.withQueryParam("payload", matchingXPath("/order/id[text()='123']"))
.willReturn(aResponse()
.withStatus(200)
.withBody("matched")));这里的matchingXPath会对payload参数的值执行XPath求值。如果请求缺少payload参数,或者参数值不是合法XML,或者XPath表达式没有命中节点,这条stub都不会匹配。对于接口测试来说,这种精确性比简单地判断“URL里包含某个字符串”更可靠。
处理命名空间和更复杂XML条件
真实项目中的XML经常带有命名空间。比如根节点声明了默认命名空间,或者使用带前缀的命名空间。此时XPath会遇到一个常见问题:看起来节点名完全一致,但匹配器却找不到节点。原因是XPath对命名空间是敏感的,默认命名空间下的元素并不等于无前缀元素。换句话说,如果XML声明了命名空间,直接用/order/id可能无法命中。
解决方式是给XPath表达式提供命名空间前缀映射。你可以为命名空间URI定义一个前缀,然后在XPath里使用这个前缀。下面示例假设XML使用了订单命名空间,并且希望匹配编号为123的订单。
{
"request": {
"method": "GET",
"urlPath": "/search",
"queryParameters": {
"payload": {
"matchesXPath": {
"expression": "/ns:order/ns:id[text()='123']",
"namespaceParameters": {
"ns": "http://ippipp.com/order"
}
}
}
}
},
"response": {
"status": 200,
"body": "matched"
}
}在Java DSL里,命名空间映射通常作为第二个参数传入。这样可以在代码中集中管理命名空间常量,避免每个测试用例里重复拼写URI。如果多个XPath条件都使用同一个命名空间,建议把映射表抽成公共变量。
import java.util.Collections;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
Map<String, String> ns = Collections.singletonMap(
"ns",
"http://ippipp.com/order"
);
stubFor(get(urlPathEqualTo("/search"))
.withQueryParam(
"payload",
matchingXPath("/ns:order/ns:id[text()='123']", ns)
)
.willReturn(aResponse().withStatus(200)));除了命名空间,XPath还可以处理更复杂的条件。比如需要同时判断状态节点和编号节点,可以写成类似/order[status='PAID']/id[text()='123']。如果编号保存在属性中,也可以使用@id这样的属性选择器。相比字符串包含,这种表达式能更准确地描述业务语义,减少因为XML格式变化导致的误匹配。
常见排错:请求明明带了XML却没命中
第一类问题通常出在URL编码。查询参数值如果包含尖括号、等号、与符号、空格等字符,必须完整编码。尤其是与符号,它在查询字符串里天然具有参数分隔含义。如果XML内容里出现了未编码的与符号,WireMock解析出来的就不再是一个完整的XML参数,而可能被拆成多个参数,最终导致XPath匹配失败。反过来,如果客户端做了双重编码,WireMock解码后看到的仍然是一串百分号编码文本,而不是XML,XPath同样无法求值。
第二类问题来自参数名和参数值细节。查询参数名是大小写敏感的,payload和Payload不会互相命中。如果同一个参数出现多次,测试语义会变得复杂,最好让业务约定使用唯一参数名承载XML。文本节点里的空格也容易被忽略,例如节点文本前后有空格时,text()='123'可能无法命中,此时可以改用normalize-space(text())='123',让XPath先做空白归一化。
第三类问题来自XML本身的可解析性。如果参数值缺少闭合标签、存在非法字符,或者被截断,XPath匹配器会认为它不是合法XML。调试时可以先把表达式简化为只匹配根节点,例如使用/order,确认参数值至少能被当作XML读取。如果连根节点都匹配不上,通常说明问题不在具体文本值,而在编码、参数解析或XML格式本身。整体来看,WireMock用XPath匹配查询参数中的XML并不复杂,真正需要重视的是请求编码是否规范、命名空间是否声明、XPath表达式是否准确表达业务条件。