概述
XMLUnit 2.x 是一个强大的 XML 测试库,特别适合在需要精确验证 XML 内容的场景中使用。它主要应用于单元测试中,帮助我们实现以下核心功能:
- 验证 XML 格式是否有效
- 检查 XML 是否包含特定信息
- 确认 XML 是否符合特定样式文档
XMLUnit 的核心优势在于:
- 灵活控制差异检测:可以自定义哪些差异类型需要关注
- 精准比较控制:能指定样式参考文档的哪些部分与测试 XML 的哪些部分进行比较
注意:本文档严格针对 XMLUnit 2.x 版本,不涉及 1.x 相关内容。测试断言将使用 Hamcrest 匹配器,建议提前熟悉 Hamcrest 的使用。
Maven 依赖配置
要在 Maven 项目中使用 XMLUnit,需要在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.2.1</version>
</dependency>
最新版本可通过 Maven 中央仓库 查询。此外还需要添加匹配器依赖:
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-matchers</artifactId>
<version>2.2.1</version>
</dependency>
最新版本见 Maven 匹配器仓库。
XML 比较
简单差异示例
当两份 XML 的内容和节点顺序完全一致时,XMLUnit 会判定为相同:
@Test
public void given2XMLS_whenIdentical_thenCorrect() {
String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
String testXml = "<struct><int>3</int><boolean>false</boolean></struct>";
assertThat(testXml, CompareMatcher.isIdenticalTo(controlXml));
}
但若节点顺序不同,即使内容相似也会被视为不同:
@Test
public void given2XMLSWithSimilarNodesButDifferentSequence_whenNotIdentical_thenCorrect() {
String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
assertThat(testXml, not(isIdenticalTo(controlXml)));
}
详细差异示例
XML 的差异检测通过 差异引擎(Difference Engine) 实现。默认情况下,引擎在发现第一个差异后就会停止比较。若要获取所有差异,需使用 Diff
类:
@Test
public void given2XMLS_whenGeneratesDifferences_thenCorrect(){
String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
Diff myDiff = DiffBuilder.compare(controlXml).withTest(testXml).build();
Iterator<Difference> iter = myDiff.getDifferences().iterator();
int size = 0;
while (iter.hasNext()) {
iter.next().toString();
size++;
}
assertThat(size, greaterThan(1));
}
差异详情示例输出:
Expected element tag name 'int' but was 'boolean' -
comparing <int...> at /struct[1]/int[1] to <boolean...>
at /struct[1]/boolean[1] (DIFFERENT)
Expected text value '3' but was 'false' -
comparing <int ...>3</int> at /struct[1]/int[1]/text()[1] to
<boolean ...>false</boolean> at /struct[1]/boolean[1]/text()[1] (DIFFERENT)
Expected element tag name 'boolean' but was 'int' -
comparing <boolean...> at /struct[1]/boolean[1]
to <int...> at /struct[1]/int[1] (DIFFERENT)
Expected text value 'false' but was '3' -
comparing <boolean ...>false</boolean> at /struct[1]/boolean[1]/text()[1]
to <int ...>3</int> at /struct[1]/int[1]/text()[1] (DIFFERENT)
若需强制引擎在首次差异后停止,可使用 ComparisonController
:
@Test
public void given2XMLS_whenGeneratesOneDifference_thenCorrect(){
String myControlXML = "<struct><int>3</int><boolean>false</boolean></struct>";
String myTestXML = "<struct><boolean>false</boolean><int>3</int></struct>";
Diff myDiff = DiffBuilder
.compare(myControlXML)
.withTest(myTestXML)
.withComparisonController(ComparisonControllers.StopWhenDifferent)
.build();
Iterator<Difference> iter = myDiff.getDifferences().iterator();
int size = 0;
while (iter.hasNext()) {
iter.next().toString();
size++;
}
assertThat(size, equalTo(1));
}
此时差异输出更简洁:
Expected element tag name 'int' but was 'boolean' -
comparing <int...> at /struct[1]/int[1]
to <boolean...> at /struct[1]/boolean[1] (DIFFERENT)
输入源处理
XMLUnit 支持多种输入源,通过 Input
类的静态方法实现:
从文件读取
@Test
public void givenFileSource_whenAbleToInput_thenCorrect() {
ClassLoader classLoader = getClass().getClassLoader();
String testPath = classLoader.getResource("test.xml").getPath();
String controlPath = classLoader.getResource("control.xml").getPath();
assertThat(
Input.fromFile(testPath), isSimilarTo(Input.fromFile(controlPath)));
}
从字符串读取
@Test
public void givenStringSource_whenAbleToInput_thenCorrect() {
String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
String testXml = "<struct><int>3</int><boolean>false</boolean></struct>";
assertThat(
Input.fromString(testXml),isSimilarTo(Input.fromString(controlXml)));
}
从流读取
@Test
public void givenStreamAsSource_whenAbleToInput_thenCorrect() {
assertThat(Input.fromStream(XMLUnitTests.class
.getResourceAsStream("/test.xml")),
isSimilarTo(
Input.fromStream(XMLUnitTests.class
.getResourceAsStream("/control.xml"))));
}
通用输入源
使用 Input.from(Object)
可自动解析多种输入类型:
// 文件输入
@Test
public void givenFileSourceAsObject_whenAbleToInput_thenCorrect() {
ClassLoader classLoader = getClass().getClassLoader();
assertThat(
Input.from(new File(classLoader.getResource("test.xml").getFile())),
isSimilarTo(Input.from(new File(classLoader.getResource("control.xml").getFile()))));
}
// 字符串输入
@Test
public void givenStringSourceAsObject_whenAbleToInput_thenCorrect() {
assertThat(
Input.from("<struct><int>3</int><boolean>false</boolean></struct>"),
isSimilarTo(Input.from("<struct><int>3</int><boolean>false</boolean></struct>")));
}
// 流输入
@Test
public void givenStreamAsObject_whenAbleToInput_thenCorrect() {
assertThat(
Input.from(XMLUnitTest.class.getResourceAsStream("/test.xml")),
isSimilarTo(Input.from(XMLUnitTest.class.getResourceAsStream("/control.xml"))));
}
特定节点比较
对于节点顺序不同的相似 XML,默认比较会失败。这是因为 XMLUnit 默认按深度优先顺序匹配节点:
@Test
public void given2XMLS_whenSimilar_thenCorrect() {
String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
assertThat(testXml, isSimilarTo(controlXml)); // 默认失败
}
错误信息示例:
java.lang.AssertionError:
Expected: Expected element tag name 'int' but was 'boolean' -
comparing <int...> at /struct[1]/int[1] to <boolean...> at /struct[1]/boolean[1]:
<int>3</int>
but: result was:
<boolean>false</boolean>
解决方案是使用 DefaultNodeMatcher
和 ElementSelectors
:
@Test
public void given2XMLS_whenSimilar_thenCorrect() {
String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
assertThat(testXml,
isSimilarTo(controlXml).withNodeMatcher(
new DefaultNodeMatcher(ElementSelectors.byName)));
}
或者使用 Diff
方式:
@Test
public void given2XMLs_whenSimilarWithDiff_thenCorrect() throws Exception {
String myControlXML = "<struct><int>3</int><boolean>false</boolean></struct>";
String myTestXML = "<struct><boolean>false</boolean><int>3</int></struct>";
Diff myDiffSimilar = DiffBuilder.compare(myControlXML).withTest(myTestXML)
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName))
.checkForSimilar().build();
assertFalse("XML similar " + myDiffSimilar.toString(),
myDiffSimilar.hasDifferences());
}
自定义差异评估器
DifferenceEvaluator
用于决定比较结果的严重程度(相同/相似/不同)。考虑以下 XML:
<!-- control.xml -->
<a>
<b attr="abc">
</b>
</a>
<!-- test.xml -->
<a>
<b attr="xyz">
</b>
</a>
默认情况下属性值不同会导致差异:
@Test
public void given2XMLsWithDifferences_whenTestsDifferentWithoutDifferenceEvaluator_thenCorrect(){
final String control = "<a><b attr=\"abc\"></b></a>";
final String test = "<a><b attr=\"xyz\"></b></a>";
Diff myDiff = DiffBuilder.compare(control).withTest(test)
.checkForSimilar().build();
assertFalse(myDiff.toString(), myDiff.hasDifferences()); // 实际会失败
}
错误信息:
java.lang.AssertionError: Expected attribute value 'abc' but was 'xyz' -
comparing <b attr="abc"...> at /a[1]/b[1]/@attr
to <b attr="xyz"...> at /a[1]/b[1]/@attr
若需忽略特定属性差异,可自定义评估器:
public class IgnoreAttributeDifferenceEvaluator implements DifferenceEvaluator {
private String attributeName;
public IgnoreAttributeDifferenceEvaluator(String attributeName) {
this.attributeName = attributeName;
}
@Override
public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
if (outcome == ComparisonResult.EQUAL)
return outcome;
final Node controlNode = comparison.getControlDetails().getTarget();
if (controlNode instanceof Attr) {
Attr attr = (Attr) controlNode;
if (attr.getName().equals(attributeName)) {
return ComparisonResult.SIMILAR;
}
}
return outcome;
}
}
使用自定义评估器的测试:
@Test
public void given2XMLsWithDifferences_whenTestsSimilarWithDifferenceEvaluator_thenCorrect() {
final String control = "<a><b attr=\"abc\"></b></a>";
final String test = "<a><b attr=\"xyz\"></b></a>";
Diff myDiff = DiffBuilder.compare(control).withTest(test)
.withDifferenceEvaluator(new IgnoreAttributeDifferenceEvaluator("attr"))
.checkForSimilar().build();
assertFalse(myDiff.toString(), myDiff.hasDifferences()); // 现在会通过
}
XML 验证
XMLUnit 通过 Validator
类实现 XML 验证。使用 forLanguage
方法指定验证模式:
Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);
示例 XSD (students.xsd
):
<?xml version = "1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name='class'>
<xs:complexType>
<xs:sequence>
<xs:element name='student' type='StudentObject'
minOccurs='0' maxOccurs='unbounded' />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="StudentObject">
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="age" type="xs:positiveInteger" />
</xs:sequence>
<xs:attribute name='id' type='xs:positiveInteger' />
</xs:complexType>
</xs:schema>
示例 XML (students.xml
):
<?xml version = "1.0"?>
<class>
<student id="393">
<name>Rajiv</name>
<age>18</age>
</student>
<student id="493">
<name>Candie</name>
<age>19</age>
</student>
</class>
验证测试:
@Test
public void givenXml_whenValidatesAgainstXsd_thenCorrect() {
Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);
v.setSchemaSource(Input.fromStream(
XMLUnitTests.class.getResourceAsStream("/students.xsd")).build());
ValidationResult r = v.validateInstance(Input.fromStream(
XMLUnitTests.class.getResourceAsStream("/students.xml")).build());
Iterator<ValidationProblem> probs = r.getProblems().iterator();
while (probs.hasNext()) {
probs.next().toString();
}
assertTrue(r.isValid());
}
对于错误 XML (students_with_error.xml
):
<?xml version = "1.0"?>
<class>
<studet id="393"> <!-- 错误标签 -->
<name>Rajiv</name>
<age>18</age>
</student>
<studet id="493">
<name>Candie</name>
<age>19</age>
</student>
</class>
错误捕获测试:
@Test
public void givenXmlWithErrors_whenReturnsValidationProblems_thenCorrect() {
Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);
v.setSchemaSource(Input.fromStream(
XMLUnitTests.class.getResourceAsStream("/students.xsd")).build());
ValidationResult r = v.validateInstance(Input.fromStream(
XMLUnitTests.class.getResourceAsStream("/students_with_error.xml")).build());
Iterator<ValidationProblem> probs = r.getProblems().iterator();
int count = 0;
while (probs.hasNext()) {
count++;
probs.next().toString();
}
assertTrue(count > 0);
}
错误输出示例:
ValidationProblem { line=3, column=19, type=ERROR,message='cvc-complex-type.2.4.a:
Invalid content was found starting with element 'studet'.
One of '{student}' is expected.' }
ValidationProblem { line=6, column=4, type=ERROR, message='The element type "studet"
must be terminated by the matching end-tag "</studet>".' }
XPath 支持
XMLUnit 提供丰富的 XPath 操作功能。示例 XML (teachers.xml
):
<teachers>
<teacher department="science" id='309'>
<subject>math</subject>
<subject>physics</subject>
</teacher>
<teacher department="arts" id='310'>
<subject>political education</subject>
<subject>english</subject>
</teacher>
</teachers>
节点检索与验证
@Test
public void givenXPath_whenAbleToRetrieveNodes_thenCorrect() {
Iterable<Node> i = new JAXPXPathEngine()
.selectNodes("//teacher", Input.fromFile(new File("teachers.xml")).build());
assertNotNull(i);
int count = 0;
for (Iterator<Node> it = i.iterator(); it.hasNext();) {
count++;
Node node = it.next();
assertEquals("teacher", node.getNodeName());
NamedNodeMap map = node.getAttributes();
assertEquals("department", map.item(0).getNodeName());
assertEquals("id", map.item(1).getNodeName());
assertEquals("teacher", node.getNodeName());
}
assertEquals(2, count);
}
路径存在性验证
@Test
public void givenXmlSource_whenAbleToValidateExistingXPath_thenCorrect() {
assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//teachers"));
assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//teacher"));
assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//subject"));
assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//@department"));
}
路径不存在性验证
@Test
public void givenXmlSource_whenFailsToValidateInExistentXPath_thenCorrect() {
assertThat(Input.fromFile(new File("teachers.xml")), not(hasXPath("//sujet")));
}
适用场景:当文档包含大量固定内容,仅少量动态生成内容时,XPath 验证特别高效。
总结
本文系统介绍了 XMLUnit 2.x 的核心功能及其实战应用,涵盖:
- XML 差异检测与比较策略
- 多源输入处理技术
- 自定义节点匹配逻辑
- 差异评估器定制方案
- XML 模式验证方法
- XPath 精准定位技巧
完整示例代码可在 XMLUnit GitHub 项目 中获取。掌握这些技术将显著提升 XML 处理的测试效率和可靠性。