在许多情况下,软件都会创建XML输出。 XML文档用于不同应用程序之间的数据交换,Web应用程序创建(X)HTML输出或使用少量XML代码段响应AJAX请求。 在许多用例中,都会生成XML,并且必须与应用程序的任何其他部分一样对输出进行测试。
有几种测试生成的XML的方法,并且每种方法在隔离使用时都有其缺陷。
例如,您可以:
- 根据DTD,XML Schema或其他任何语法替代方法验证生成的XML。 不幸的是,这样的语法在您的文档中并不总是存在,即使存在,每个测试也只会测试输出的结构,而不会测试其内容。
- 将生成的输出与预期结果进行比较。 不幸的是,表示相同信息树结构的两个XML文档的序列化格式可能会大不相同。 没有任何子节点的元素可以折叠为空元素,或者仍然使用开始和结束标签进行序列化,例如,空格或字符编码可能有所不同。
- 使用XPath查询来提取所生成文档的部分内容并对其值进行断言。 如果需要测试的生成内容量很大,这可能会变得很乏味。
- 以编程方式遍历文档(例如,使用其DOM对象模型)并声明每个节点的内容。 用这种方式编写的测试非常具体,当输出结构更改时可能需要进行更大的调整。
另外,用于这两个任务的现有API通常很不方便。 例如,在JAXP 1.3之前的Java(即Java SE 5之前)中,仅当将文档从字节流或字符流解析为DOM文档实例或SAX流时,才可以针对DTD或XML模式验证文档。事件。
XMLUnit
XMLUnit是根据BSD许可获得许可的开源项目。 它提供了一个相互联系的类的小型图书馆,该图书馆简化了上一节中概述的测试XML片段的每种不同方法。 提供了特殊的API来简化使用J / NUnit编写单元测试的过程,但是该库本身完全可用,完全不需要任何测试框架。
有Java和.NET版本的XMLUnit,但是Java版本更加成熟并提供更多功能。 本文仅关注Java版本,所有示例都将使用Java。
XMLUnit由Tim Bacon和Jeff Martin于2001年成立,并作为其自己项目的测试框架进行开发。 Java的XMLUnit的第一个稳定版本于2003年3月发布。在此1.0版本发布之后的四年中,XMLUnit已在许多开源项目和闭源项目中使用,但其积极的开发却停滞了。
同时,XML生态系统发生了变化。 XMLUnit 1.0的验证类非常着重于DTD,后来才支持XML Schema。 同样,作为XMLUnit 1.0一部分的简单XPath引擎根本不支持XML命名空间。
在2006年秋季,XMLUnit的开发再次得到了支持,XMLUnit 1.1的第一个beta版已经在2007年4月发布,并且最终版本有望在不久之后发布。 在XMLUnit邮件列表上,已经有关于此版本之外的进一步开发的讨论。
本文其余部分中的示例使用XMLUnit 1.1,但是许多示例也将应用于XMLUnit 1.0。
提供XML作为XMLUnit的输入
XMLUnit的API将使用几种不同的形式接受“ XML片段”作为输入。 在大多数情况下,它们可以作为InputStreams,Reader,String,InputSources或易于解析的DOM Document实例提供。
XMLUnit还提供了一个Transform类,该类可用于将XSLT转换应用于现有输入(使用上述提供的格式之一),并在进一步的测试中使用此转换的输出。
Transform tr = new Transform("
",
new File("xml/example1.xsl"));
Document d = tr.getResultDocument();
assertEquals("example1", d.getDocumentElement().getTagName());
样式表包含类似
示例1:使用变换测试XSLT变换的结果
验证XML
XMLUnit可以根据DTD或W3C XML Schema验证XML文档。 更高版本的XMLUnit将利用JAXP 1.3中添加的javax.xml.validation包,因此也可能提供对RELAX NG,Schematron或其他语法的验证。
对于任何一种验证形式,都使用XMLUnit的Validator类。
根据W3C XML架构进行验证
由于DTD验证是XMLUnit的默认设置,因此必须通过将Validator的useXMLSchema属性设置为true来显式启用模式验证。
为了针对XML Schema进行验证,被测文档必须使用Schema的URI声明XML名称空间。 该文档还可以提供schemaLocation属性,该属性告诉XML解析器在哪里可以找到该模式的定义。
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="file:///opt/schemas/example.com/order.xsd"/>
示例2:具有名称空间声明和schemaLocation属性的XML文档
如果未提供schemaLocation,则XML解析器将尝试使用名称空间的URI作为URL,并从那里读取模式的定义。
不幸的是,在许多情况下,既不提供schemaLocation属性也不使用有效的URL作为名称空间的URI是不可行的。 生成的XML输出可以在其他机器上(例如在客户的站点上)进行处理,以便本地文件引用不起作用,并且该机器可能没有网络访问权限,因此任何公共http URL也不起作用。
幸运的是,JAXP 1.2(即Java 1.4)提供了一个很好的隐藏功能,可以以编程方式提供架构位置。 模式的位置可以指定为文件或URL,甚至可以指定为字节块。
String example = "
"
+ "
"
+ "
";
Validator v = new Validator(example);
v.useXMLSchema(true);
v.setJAXP12SchemaSource(new File("xml/example3.xsd"));
assertTrue(v.toString(), v.isValid());
示例3:根据W3C XML模式验证XML文档,以编程方式提供schemaLocation
XMLUnit当前仅支持对XML Schema实例文档的验证,但是您不能验证Schema定义本身就是有效的XML Schema。 计划在将来的版本中大大扩展支持。
针对DTD进行验证
XMLUnit在许多不同的场景中都支持DTD验证。 在最基本的情况下,被测文档包含提供系统标识符的文档类型声明。
"file:///opt/schemas/example.com/order.xsd" >
示例4:带有DOCTYPE声明和SYSTEM和PUBLIC标识符的XML文档
在这种情况下,解析器将使用给定的标识符查找文档。
出于与XML Schema部分中概述的相同原因,这可能不是所希望的,因此XMLUnit允许您提供自己的SYSTEM标识符。 如果这样做,它甚至会覆盖现有的SYSTEM标识符。 通过指定SYSTEM标识符,您还可以验证根本不包含任何DOCTYPE声明的文档。
String example = "
+ " \"http://example.com/order\">"
+ "
"
+ "
"
+ "
";
Validator v = new Validator(example,
new File("xml/example5.dtd")
.toURI().toURL().toString());
assertTrue(v.toString(), v.isValid());
示例5:根据DTD验证XML文档,以编程方式提供位置
或者,您可以指定一个SAX EntityResolver,它将为DTD提供位置。 例如,这可以用于使用Apache的XML Resolver库将解析度推迟到OASIS目录中。
String example = "
+ " \"http://example.com/order\">"
+ "
"
+ "
"
+ "
";
Validator v = new Validator(example);
XMLUnit.setControlEntityResolver(new CatalogResolver());
assertTrue(v.toString(), v.isValid());
像这样的目录
xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
uri="example5.dtd"/>
示例6:使用OASIS目录解析DTD位置
比较XML
当XMLUnit比较两段XML时,结果可能是以下三种状态之一:
- 这两个XML完全相同
- 这两个XML相似
- 这两段XML是不同的
XMLUnit将它检测到的每种差异分类为可恢复或不可恢复(请参见下文)。 仅在完全没有差异的情况下,XML片段才是相同的。 如果发现的所有差异都是可以恢复的,则说这些文档是相似的,否则它们是不同的。
默认情况下,XMLUnit将仅考虑几种可恢复的差异。 例如,如果两个文档对相同的名称空间使用不同的前缀,则它们被视为相似但不相同。 可在XMLUnit的用户指南中找到所有检测到的差异的完整列表。 似乎令人惊讶的一件事是,如果XMLUnit包含两个具有不同顺序的相同元素,则它们会将两个文档视为相似。
String expected = "
";
String actual = "
";
Diff d = new Diff(expected, actual);
assertTrue(d.identical());
actual = "" + actual;
d = new Diff(expected, actual);
assertFalse(d.identical()); assertTrue(d.similar());
XMLAssert.assertXMLEqual(expected, actual);
示例7:比较两段XML
示例7的最后一行显示了XMLAssert类提供的便捷方法assertXMLEqual。 有几种用于XML比较的重载方法以及XMLUnit支持的其他XML测试方案,这些方法简化了API的工作,从而使XMLUnit和JUnit 3.x的结合更加明显。 请注意,“ assertXMLEqual”有点用词不当,因为该方法提供的是相似性(而非相等性)测试。 如果两个XML相似但不相同,则assertXMLIdentical将失败。
XMLUnit提供了几个扩展点,这些扩展点提供了对比较结果的更多控制。
差异监听器
通过提供DifferenceListener接口的实现,您可以自己确定哪种差异类型在您的上下文中很重要。 您可以将元素顺序中的差异“升级”为无法恢复,或选择忽略注释中的差异。
String expected = "
";
String actual = "
";
Diff d = new Diff(expected, actual);
assertFalse(d.similar());
d = new Diff(expected, actual);
d.overrideDifferenceListener(new DifferenceListener() {
public int differenceFound(Difference difference) {
if (difference.getId()
== DifferenceConstants.COMMENT_VALUE_ID)
{
return RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
}
return RETURN_ACCEPT_DIFFERENCE;
}
public void skippedComparison(Node control, Node test)
});
assertTrue(d.identical());
示例8a:比较两段XML,忽略注释中的差异
因为忽略注释是一种常见的要求,所以XMLUnit提供了一个简单的选项来完全忽略它们。
String expected = "
";
String actual = "
";
Diff d = new Diff(expected, actual);
assertFalse(d.similar());
XMLUnit.setIgnoreComments(true);
d = new Diff(expected, actual)
assertTrue(d.identical());
例8b:比较两段XML,忽略注释中的差异
ElementQualifier
鉴于XMLUnit并不认为元素的顺序很重要,因此并不总是很明显需要将给定节点的哪些子元素相互比较。 默认情况下,XMLUnit将尝试将具有相同标签名称的元素相互比较,但是在某些情况下,这可能会导致不良结果。
text
some other text
some other text
text
示例9:当元素的标记名称不够好时
在上面的示例中,除了标签名称之外,还需要使用元素的文本内容来选择正确的元素; 这可以使用ElementNameAndTextQualifier实现。
String expected = "
"
+ "
text
"
+ "
some other text
"
+ "
";
String actual = "
"
+ "
some other text
"
+ "
text
"
+ "
";
Diff d = new Diff(expected, actual);
assertFalse(d.similar());
d = new Diff(expected, actual);
d.overrideElementQualifier(new ElementNameAndTextQualifier())
assertTrue(d.similar());
示例10:使用ElementNameAndTextQualifier
ElementNameAndTextQualifier是ElementQualifier接口的几种实现之一,该接口是XMLUnit分发的一部分。 此外,如果标识可比较节点的逻辑过于具体,则可以提供自己的实现。
详细差异
到目前为止,这些示例仅验证了两个XML是否相同。 比较两个XML片段时,另一个用例是枚举它们之间的所有差异。 这是DetailedDiff的任务。
String expected = "
"
+ "
"
+ "
text
"
+ "
some other text
"
+ "
";
String actual = "
"
+ "
some other text
"
+ "
text
"
+ "
";
DetailedDiff dd = new DetailedDiff(new Diff(expected, actual));
List l = dd.getAllDifferences();
for (Iterator i = l.iterator(); i.hasNext(); ) {
Difference d = (Difference) i.next();
System.err.println(d);
}
assertEquals(6, l.size());
示例11:查找两段XML之间的所有差异
DetailsDiff是Diff的子类,因此它也可以用于将两个文档分类为相似或不同。 但是,与DetailedDiff不同,Diff会在遇到不可恢复的差异时立即停止比较过程,因此,如果不需要查找所有差异,则应使用Diff来提高性能。
Diff和DetailedDiff都按需计算两段XML之间的差异,并缓存结果。 这意味着如果要使用一组不同的选项重复比较,则需要创建一个新的Diff实例。
更多配置选项
XMLUnit的大多数配置都是通过XMLUnit类的静态方法完成的。 默认值的任何更改都将应用,直到显式重置这些值为止。 如果要在单元测试用例中修改默认设置,则最好在每次测试后重置它们(例如,如果使用JUnit 3.x,则在tearDown方法中),以使不同的测试不会相互影响。
您最想更改的选项是空白处理。
String expected = "
";
String actual = "
\n"
+ "
\n"
+ "
";
Diff d = new Diff(expected, actual);
assertFalse(d.similar());
XMLUnit.setIgnoreWhitespace(true);
d = new Diff(expected, actual);
assertTrue(d.identical());
示例12:元素内容空白
在上面的示例中,这两种XML被认为是不同的,因为第一个元素包含嵌套文本(换行符),而第二个元素中则没有。
通过将XMLUnit的ignoreWhitespace属性设置为true,可以抑制差异,并且两个文档将被视为相同。
其他选项包括忽略注释或将CDATA节和“常规”嵌套文本视为一种内容。 即在下面的示例中,两个断言都将通过。
String expected = "
例13:比较CDATA节和“普通”文本
XPath测试
传统上,XMLUnit使用自己的基于XSLT的XPath引擎。 如果XMLUnit 1.1检测到运行时可用,它将优先支持JAXP 1.3的javax.xml.xpath,但如果不支持,则回退到内部。
不管在后台使用哪种XPath引擎,XMLUnit都支持获得将XPath表达式作为DOM NodeList或String应用于XML的结果。 通常,如果您希望只有一个结果并且该结果是属性或嵌套元素文本的值,则后一种形式更合适。
XpathEngine eng = XMLUnit.newXpathEngine();
String input = "
";
Document doc = XMLUnit.buildControlDocument(input);
assertEquals("1", eng.evaluate("/order/item[1]/@id", doc));
XMLAssert.assertXpathExists("/order/item[1]/@id", input);
XMLAssert.assertXpathEvaluatesTo("1", "/order/item[1]/@id", input);
assertEquals(2, eng.getMatchingNodes("/order/item", doc).getLength());
示例14:测试XPath查询
XMLUnit 1.0的XPath引擎在命名空间文档上无法正常工作,尤其是如果一个文档一次包含多个命名空间时。 XMLUnit 1.1引入了NamespaceContext接口和一个简单的基于Map的实现,该实现有助于将前缀映射到URL。
String input = "
"
+ "
"
+ "
";
Document doc = XMLUnit.buildControlDocument(input);
HashMap m = new HashMap();
m.put("x", "urn:order");
SimpleNamespaceContext ctx = new SimpleNamespaceContext(m);
XMLUnit.setXpathNamespaceContext(ctx);
XpathEngine eng = XMLUnit.newXpathEngine();
assertEquals("1", eng.evaluate("/x:order/x:item[1]/@id", doc));
XMLAssert.assertXpathExists("/x:order/x:item[1]/@id", input);
XMLAssert.assertXpathEvaluatesTo("1", "/x:order/x:item[1]/@id", input);
assertEquals(2, eng.getMatchingNodes("/x:order/x:item", doc).getLength());
示例15:在命名空间文档上测试XPath查询
使用NamespaceContext时,请记住只有名称空间的URI是相关的,而前缀则不相关。 NamespaceContexts中提供的前缀适用于XPath选择器,而不适用于文档本身。 在文档内部,前缀将被完全忽略。
DOM树上的程序测试
有时,很难通过将生成的XML与预定义的结果进行比较来测试生成的XML,并且使用XPath测试单个节点会变得非常复杂,因为必须单独测试太多的节点。
对于这种情况,XMLUnit提供了一种非常强大的测试方法,使您可以使用简单的界面以编程方式测试所生成XML的每个节点。
在下面的示例中,假定所生成的XML在所有项目元素的id属性中都包含一个GUID(表示为八个十六进制数字)。 该测试将验证属性值是否符合预期格式,并且ID对于生成的文档而言是唯一的。
private class GuidTester extends AbstractNodeTester {
private static final String pattern = "[0-9,a-f]{8}";
private Set visitedIds = new HashSet();
public void testElement(Element element) throws NodeTestException {
if (element.getTagName().equals("item")) {
String idAttr = element.getAttribute("id");
if (!idAttr.matches(pattern)) {
throw new NodeTestException("id attribute: " + idAttr
+ " is not in correct format");
}
if (visitedIds.contains(idAttr)) {
throw new NodeTestException("id attribute: " + idAttr
+ " is not unique");
}
visitedIds.add(idAttr);
}
}
}
public void testUniqueIds() throws Exception {
String works = "
"
+ "
"
+ "
"
+ "
";
NodeTest nt = new NodeTest(works);
nt.performTest(new GuidTester(), Node.ELEMENT_NODE);
String badPattern = "
"
+ "
"
+ "
";
nt = new NodeTest(badPattern);
try {
nt.performTest(new GuidTester(), Node.ELEMENT_NODE);
fail("expected exception");
} catch (NodeTestException ex) {
assertTrue(ex.getMessage().indexOf("format") > -1);
}
String notUnique = "
"
+ "
"
+ "
"
+ "
";
nt = new NodeTest(notUnique);
try {
nt.performTest(new GuidTester(), Node.ELEMENT_NODE);
fail("expected exception");
} catch (NodeTestException ex) {
assertTrue(ex.getMessage().indexOf("not unique") > -1);
}
}
示例16:使用NodeTester验证XML文档
放在一起
任何创建XML的软件都应该对其输出进行测试,就像它需要对其功能的任何其他部分进行测试一样。
测试XML可以采用几种不同的方法,有时将不止一种方法结合使用可获得最佳结果。 对于简单的情况,将生成的输出与预期的输出进行比较可能就足够了;对于更复杂的情况,应将输出结构的形式验证与包含XPath查询(用于小型输出)或编程测试的内容测试结合起来。
用Java处理XML的API通常不方便使用,XMLUnit为本文概述的所有测试方法提供了简化的API。
本文无法涵盖XMLUnit的所有方面。 例如,通过特殊DocumentBuilder和SAXParser类的实现,对格式不是XMLHTML文档的支持。 除了某些示例中显示的XMLAssert类之外,还有一个XMLTestCase类,该类扩展了JUnit的TestCase并提供了与XMLAssert类似的方法。
您可以在项目的网站及其《 用户指南》中了解有关XMLUnit的更多信息。
Stefan Bodewig是位于德国埃森的WebOne Informatik GmbH的首席开发人员,负责基于Microsoft .NET平台的应用程序的体系结构和开发。 Stefan还是几个开源项目的贡献者,包括XMLUnit和Apache Ant。