忽略元素顺序校验XML文件相等:XMLUnit失效后的方案咨询
XML全层级排序后比较的解决方案(无需预先知晓结构)
现有两段仅<elem1>元素顺序不同的XML,需要编写测试证明二者相等。使用XMLUnit工具时,尝试ElementSelectors.byNameAndText和ElementSelectors.byNameAndAllAttributes两种匹配规则均未成功,且现有方案多需要预先知晓XML结构,求无需预定义结构、可对XML所有层级元素统一排序后再比较的方法及可用库。
待比较的XML示例:
var xml1 = """ <root> <elem1> <elem2 attr="a"/> </elem1> <elem1> <elem2 attr="b"/> </elem1> </root> """; var xml2 = """ <root> <elem1> <elem2 attr="b"/> </elem1> <elem1> <elem2 attr="a"/> </elem1> </root> """;
此前尝试的测试代码:
// 尝试规则1 assertThat(xml1, isSimilarTo(xml2) .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))); // 尝试规则2 assertThat(xml1, isSimilarTo(xml2) .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndAllAttributes)));
解决方案
1. 基于XMLUnit实现全层级动态排序匹配
无需预定义XML结构,通过自定义ElementSelector结合递归排序逻辑,让XMLUnit能自动匹配排序后的子元素:
自定义匹配工具类
import org.xmlunit.diff.*; import org.w3c.dom.Element; import java.util.*; import java.util.stream.Collectors; public class XmlFullHierarchyComparator { // 自定义递归匹配选择器:自动排序子元素后比较 public static ElementSelector recursiveByNameAndContent() { return (e1, e2) -> { // 先匹配元素名 if (!e1.getNodeName().equals(e2.getNodeName())) { return false; } // 匹配属性 if (!compareAttributes(e1, e2)) { return false; } // 匹配文本内容(去除空白) String text1 = Optional.ofNullable(e1.getTextContent()).orElse("").trim(); String text2 = Optional.ofNullable(e2.getTextContent()).orElse("").trim(); if (!text1.equals(text2)) { return false; } // 递归排序并匹配子元素 List<Element> sortedChildren1 = getSortedChildElements(e1); List<Element> sortedChildren2 = getSortedChildElements(e2); if (sortedChildren1.size() != sortedChildren2.size()) { return false; } for (int i = 0; i < sortedChildren1.size(); i++) { if (!recursiveByNameAndContent().match(sortedChildren1.get(i), sortedChildren2.get(i))) { return false; } } return true; }; } // 按「元素名+属性+文本」排序子元素 private static List<Element> getSortedChildElements(Element parent) { return Arrays.stream(parent.getElementsByTagName("*")) .map(node -> (Element) node) .sorted((e1, e2) -> { int nameCompare = e1.getNodeName().compareTo(e2.getNodeName()); if (nameCompare != 0) return nameCompare; String attrs1 = getSortedAttributesString(e1); String attrs2 = getSortedAttributesString(e2); int attrCompare = attrs1.compareTo(attrs2); if (attrCompare != 0) return attrCompare; String text1 = Optional.ofNullable(e1.getTextContent()).orElse("").trim(); String text2 = Optional.ofNullable(e2.getTextContent()).orElse("").trim(); return text1.compareTo(text2); }) .collect(Collectors.toList()); } // 将属性按键排序后拼接成字符串,用于排序比较 private static String getSortedAttributesString(Element elem) { return elem.getAttributes().getLength() == 0 ? "" : Arrays.stream(elem.getAttributes()) .map(node -> (org.w3c.dom.Attr) node) .sorted(Comparator.comparing(org.w3c.dom.Attr::getName)) .map(attr -> attr.getName() + "=" + attr.getValue()) .collect(Collectors.joining(";")); } // 比较两个元素的属性是否完全一致 private static boolean compareAttributes(Element e1, Element e2) { if (e1.getAttributes().getLength() != e2.getAttributes().getLength()) { return false; } return Arrays.stream(e1.getAttributes()) .map(node -> (org.w3c.dom.Attr) node) .allMatch(attr -> attr.getValue().equals(e2.getAttribute(attr.getName()))); } }
使用自定义选择器的测试代码
assertThat(xml1, isSimilarTo(xml2) .withNodeMatcher(new DefaultNodeMatcher(XmlFullHierarchyComparator.recursiveByNameAndContent())));
2. 基于DOM4J的XML归一化方案
先将两段XML全层级排序生成结构完全一致的归一化XML,再直接比较字符串:
归一化工具类
import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import java.util.*; public class XmlNormalizer { // 生成全层级排序后的归一化XML字符串 public static String normalizeXml(String xml) throws DocumentException { Document doc = DocumentHelper.parseText(xml); sortElementsRecursively(doc.getRootElement()); return doc.asXML(); } // 递归排序所有层级的元素 private static void sortElementsRecursively(Element element) { // 排序当前元素的子元素 List<Element> children = new ArrayList<>(element.elements()); children.sort((e1, e2) -> { int nameCompare = e1.getName().compareTo(e2.getName()); if (nameCompare != 0) return nameCompare; String attrs1 = getSortedAttributesString(e1); String attrs2 = getSortedAttributesString(e2); int attrCompare = attrs1.compareTo(attrs2); if (attrCompare != 0) return attrCompare; return e1.getTextTrim().compareTo(e2.getTextTrim()); }); // 替换为排序后的子元素 element.elements().clear(); element.add(children); // 递归处理子元素的子节点 for (Element child : children) { sortElementsRecursively(child); } } // 将属性按键排序后拼接成字符串 private static String getSortedAttributesString(Element elem) { if (elem.attributeCount() == 0) return ""; List<org.dom4j.Attribute> attrs = new ArrayList<>(elem.attributes()); attrs.sort(Comparator.comparing(org.dom4j.Attribute::getName)); StringBuilder sb = new StringBuilder(); for (org.dom4j.Attribute attr : attrs) { sb.append(attr.getName()).append("=").append(attr.getValue()).append(";"); } return sb.toString(); } }
测试代码
String normalizedXml1 = XmlNormalizer.normalizeXml(xml1); String normalizedXml2 = XmlNormalizer.normalizeXml(xml2); assertEquals(normalizedXml1, normalizedXml2);
内容的提问来源于stack exchange,提问作者ka3ak
相关产品推荐
相关产品推荐

