You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何比较含嵌套内容的XML?XMLUnit使用问题求助

XML比较问题排查与修复

需求说明

需要比较特定结构的XML:

  • <name>为数组节点,需匹配其所有嵌套字段
  • <TOKEN>节点需忽略<generateDate>,且内部<foo>数组按名称+value属性匹配

待比较XML结构

<ROOT>
    <name>
        <name>string</name>
        <type>number</type>
        <server>string</server>
        <service>number</service>
        <user/>
        <enabled>boolean</enabled>
    </name>
    <name>
        <name>string</name>
        <type>number</type>
        <server>string</server>
        <service>number</service>
        <user/>
        <enabled>boolean</enabled>
    </name>
    ...

    <TOKEN>
        <generateDate>date</generateDate>
        <username>string</username>
        <role>string</role>
        <type value="string" anotherValue="string"/>
        <attr value="string"/>
        <foo value="string1"/>
        <foo value="string2"/>
        ... 
    </TOKEN>
</ROOT>

现有问题代码

使用xmlunit-core 2.9.0编写的比较代码,但<name>节点内容比较失败:

Diff diff = DiffBuilder.compare(expected)
        .withTest(actual)
        .ignoreWhitespace()
        .ignoreComments()
        .checkForSimilar()
        .withNodeMatcher(
                new DefaultNodeMatcher(
                        ElementSelectors.conditionalBuilder()
                                .whenElementIsNamed("name")
                                .thenUse(ElementSelectors.and(
                                        ElementSelectors.byNameAndAllAttributes,
                                        ElementSelectors.byNameAndText
                                ))
                                .whenElementIsNamed("foo")
                                .thenUse(ElementSelectors.byNameAndAttributes("value"))
                                .elseUse(ElementSelectors.byNameAndText)
                                .build()
                )
        )
        .withDifferenceEvaluator((comparison, outcome) -> {
            Node targetNode = comparison.getControlDetails().getTarget();
            String nodeName = "";
            if (targetNode != null) {
                nodeName = targetNode.getNodeName();
                if ("#text".equals(nodeName) && targetNode.getParentNode() != null) {
                    nodeName = targetNode.getParentNode().getNodeName();
                }
            }
            if (nodeName.equals("generateDate")) {
                return ComparisonResult.EQUAL;
            }
            return outcome;
        })
        .build();

问题原因与修复方案

1. <name>节点匹配逻辑错误

当前对顶层<name>节点使用ElementSelectors.and(byNameAndAllAttributes, byNameAndText),但顶层<name>本身无属性和直接文本(文本都在子节点中),导致匹配逻辑完全失效。

正确做法是递归匹配<name>的所有子节点内容,确保嵌套字段完全一致:

.whenElementIsNamed("name")
.thenUse(ElementSelectors.byRecursiveXPath("./*", ElementSelectors.byNameAndText))

2. 兜底匹配规则冲突

elseUse(ElementSelectors.byNameAndText)会对空节点(如<user/>)生效,这类节点无文本,会触发不必要的差异。需调整兜底规则,兼容空节点:

.elseUse(ElementSelectors.conditional()
        .when(ElementSelectors.hasText())
        .thenUse(ElementSelectors.byNameAndText)
        .elseUse(ElementSelectors.byName))

3. <generateDate>忽略逻辑优化

原代码仅通过control侧节点判断名称,存在test侧节点为null的情况,需同时兼容两侧节点的判断:

.withDifferenceEvaluator((comparison, outcome) -> {
    String nodeName = null;
    Node controlNode = comparison.getControlDetails().getTarget();
    Node testNode = comparison.getTestDetails().getTarget();
    
    if (controlNode != null) {
        nodeName = controlNode.getNodeName();
    } else if (testNode != null) {
        nodeName = testNode.getNodeName();
    }
    
    if ("generateDate".equals(nodeName)) {
        return ComparisonResult.EQUAL;
    }
    return outcome;
})

修复后的完整代码

Diff diff = DiffBuilder.compare(expected)
        .withTest(actual)
        .ignoreWhitespace()
        .ignoreComments()
        .checkForSimilar()
        .withNodeMatcher(
                new DefaultNodeMatcher(
                        ElementSelectors.conditionalBuilder()
                                // 匹配顶层<name>节点:递归比较所有子节点内容
                                .whenElementIsNamed("name")
                                .thenUse(ElementSelectors.byRecursiveXPath("./*", ElementSelectors.byNameAndText))
                                // 匹配<foo>节点:按名称+value属性
                                .whenElementIsNamed("foo")
                                .thenUse(ElementSelectors.byNameAndAttributes("value"))
                                // 兜底规则:有文本的节点按名称+文本匹配,空节点按名称匹配
                                .elseUse(ElementSelectors.conditional()
                                        .when(ElementSelectors.hasText())
                                        .thenUse(ElementSelectors.byNameAndText)
                                        .elseUse(ElementSelectors.byName))
                                .build()
                )
        )
        .withDifferenceEvaluator((comparison, outcome) -> {
            // 忽略generateDate节点的所有差异
            String nodeName = null;
            Node controlNode = comparison.getControlDetails().getTarget();
            Node testNode = comparison.getTestDetails().getTarget();
            
            if (controlNode != null) {
                nodeName = controlNode.getNodeName();
            } else if (testNode != null) {
                nodeName = testNode.getNodeName();
            }
            
            if ("generateDate".equals(nodeName)) {
                return ComparisonResult.EQUAL;
            }
            return outcome;
        })
        .build();

内容的提问来源于stack exchange,提问作者Andrei Manolache

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 13:44:53