如何使用 Citrus 框架验证嵌入 JSON 中的 XML?

How can I validate XML embedded in JSON using Citrus framework?

在我的 Citrus 测试中,我试图验证嵌入在 JSON 文档(WireMock 的输出)中的 XML 消息。 XML 文本需要解析,因为它包含我想忽略的时间戳。 JSON 消息的 XML 部分如下所示:

"requests": [
{
    "id": "52844d5a-59de-4288-8000-7f48fcda41e5",
    "request": {
        "body": "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"> [content omitted] </soapenv:Body></soapenv:Envelope>",
}
]

这是我的Java测试代码:

http()
    .client(wiremockClient)
    .receive()
    .response(HttpStatus.OK)
    .messageType(MessageType.JSON)
    .extractFromPayload("$.requests[0].request.body", "body")
    .payload(new ClassPathResource("output/esb/add_conf_to_cart/response2.xml"))
;

其中 respon2.xml 仅包含 XML 文本(上面显示的 JSON 消息中 "body" 的内容),以及我从 Citrus 得到的错误是

com.consol.citrus.exceptions.CitrusRuntimeException: Failed to parse JSON text
…
Caused by: net.minidev.json.parser.ParseException: Unexpected token <soapenv:Envelope xmlns:soapenv=

那么,如何验证嵌入在 JSON 文档中的 XML?

澄清

我对您当前的解决方案有两点意见:

  1. extractFromPayload(..) 将你 JSON 的 body 部分提取到名为 body 的 Citrus 变量中。这与 payload(..) 部分没有任何关系。您刚刚将 body 的 JSON 内容存储到 Citrus 变量 body 中。您可能知道也可能不知道

  2. payload(..) 方法将始终验证接收到的整个有效载荷,即整个 JSON 对象。

  3. 如果您只想验证响应的一部分,请使用JSON Path validation.validate("$.some.json.path", "someValue")

解决方案

Citrus 为您的用例提供了一个内部方法,请参阅 matchesXml() 的文档。

仅验证 XML 结构

这是最简单的解决方案:

http()
    .client(wiremockClient)
    .receive()
    .response(HttpStatus.OK)
    .messageType(MessageType.JSON)
    .validate("$.requests[0].request.body", @matchesXml('citrus:readFile('classpath:output/esb/add_conf_to_cart/response2.xml')')@)

正在验证整个 JSON 响应

在您的情况下,您只需匹配整个 JSON 结构并将方法 @matchesXml('<some><validation_xml></some></validation_xml>')@ 放入正确的 JSON 条目中:

http()
.client(wiremockClient)
.receive()
.response(HttpStatus.OK)
.messageType(MessageType.JSON)
.payload("{\n"+
    "  \"requests\": {\n"+
    "    \"id\": \"52844d5a-59de-4288-8000-7f48fcda41e5\",\n"+
    "    \"request\": {\n"+
    "      \"body\": \"@matchesXml('<soapenv:Envelope xmlns:soapenv=\\"http://schemas.xmlsoap.org/soap/envelope/\\"> [content omitted] </soapenv:Body></soapenv:Envelope>')@\"\n"+
    "    }\n"+
    "  }\n"+
    "}");

备注

  • 放入 payload 的字符串必须有效 JSON,即 JSON 值中的引号必须转义。
  • Java 字符串中的引号也必须转义,因此我们必须写成 \\"
  • Citrus 首先将 payload 字符串解析为 JSON,并在内部将其保存为 JSON 对象,其中转义引号未转义。然后它将此字符串传递给验证函数 matchesXml.
  • 您当前的示例无效 [​​=112=],因为标签 </soapenv:Body> 从未打开过
  • 将负载请求放入单独的文件中要容易得多,请注意该文件仍然必须有效 JSON,即用一个反斜杠转义引号 \"
  • 您可能希望从单独的文件中读取 XML 内容。在这种情况下,您可以使用 Citrus 的 citrus:readFile() 函数
  • 对于 payload,您仍然需要在 XML 中转义引号。你可以用 citrus:translate() 来做到这一点:citrus:translate('citrus:readFile('classpath:some/path/to/response.xml')', '\"', '\"')
  • 转义非常棘手,因为我们正在处理 Java、JSON 和 XML
  • 中特殊的引号和反斜杠
  • 它应该按如下方式工作:

创建文件response_validation.json:

{
  "requests": {
    "id": "52844d5a-59de-4288-8000-7f48fcda41e5",
    "request": {
      "body": "@matchesXml('${jsonEscapedXmlInput}')@"
    }
  }
}

在您的测试用例中创建 Citrus 变量 jsonEscapedXmlInput,它读取并转义 XML 文件:

variable("jsonEscapedXml", "citrus:translate('citrus:readFile('classpath:output/esb/add_conf_to_cart/response2.xml')', '\\"', '\\\"')")

那就用吧

http()
    .client(wiremockClient)
    .receive()
    .response(HttpStatus.OK)
    .messageType(MessageType.JSON)
    .payload(new ClassPathResource("classpath:validation/response_validation.json"))
;

使用 gucce 的解决方案,我终于找到了验证 XML 的正确语法。代码现在看起来像这样:

        variable("jsonXml", "'citrus:readFile('classpath:output/esb/add_conf_to_cart/response2.xml')'");

        http()
                .client(wiremockClient)
                .send()
                .get("/__admin/requests")
                .accept("application/json");

        HttpClientResponseActionBuilder body = http()
                .client(wiremockClient)
                .receive()
                .response(HttpStatus.OK)
                .messageType(MessageType.JSON)
                .validate("$.requests[0].request.body", "@matchesXml(${jsonXml})@")
                ;

我犯的错误首先是忘记了第二个@,然后是在匹配的参数周围添加了单引号XML。所以

.validate("$.requests[0].request.body", "@matchesXml('${jsonXml}')@")

行不通,但

.validate("$.requests[0].request.body", "@matchesXml(${jsonXml})@")

会完成任务的。

另一种解决方案使用 AbstractTestAction 的实现来调用在 Citrus 上下文中执行验证的方法:

    http()
            .client(wiremockClient)
            .receive()
            .response(HttpStatus.OK)
            .messageType(MessageType.JSON)
            .extractFromPayload("$.requests[0].request.body", "body")
    ;
    action(action(new DummyAction(new File(EXPECTED_OUTPUT_FILE)));

action() 调用注册 DummyAction 以执行验证。在我的例子中,它需要一个文件来验证通过 http().extractFromPayload("$.requests[0].request.body", "body")

Class DummyAction 看起来像这样:

class DummyAction extends AbstractTestAction {

    private File expectedOutputFile;

    public DummyAction(File expectedOutputFile) {
        this.expectedOutputFile = expectedOutputFile;
    }

    @Override
    public void doExecute(TestContext testContext) {
        String body = testContext.getVariable("body");
        Assert.assertTrue(body.startsWith("<soapenv:Envelope "), "Request should start with '<soapenv:Envelope '");
    }
}

doExecute() 可以访问测试上下文中可用的变量。因此 extractFromPayload() 在上下文中放置了一个名为 'body' 的变量,随后在 doExecute() 中访问该变量。 这个解决方案允许任意 Java 代码到 运行,在我的例子中,从 SOAP 信封中提取 XML 文档,埋在 JSON 文档中。