在软件开发周期中,有时需要验证XML文档的结构或内容。 无论您要构建哪种类型的应用程序,测试XML文档都会带来一些挑战,尤其是在没有工具来简化该过程的情况下。
本月,我将首先向您展示为什么您不想使用String比较来验证XML文档的结构和内容。 然后,我将介绍XMLUnit,这是由Java开发人员创建并为Java开发人员使用的XML验证工具,并向您展示如何使用它来验证XML文档。
提高代码质量
不要错过安德鲁(Andrew)随附的讨论论坛 ,以获取有关代码指标,测试框架以及编写注重质量的代码的帮助。
旧的String比较好
首先,让我们假设您已经构建了一个应用程序,该应用程序输出代表对象相关性报告的XML文档。 对于给定的类集合和相应的过滤器,将生成一个报告,该报告输出一个类及其类相关性(请考虑导入)。
清单1显示了给定类com.acme.web.Widget和com.acme.web.Account类的列表的报告,其中设置了过滤器以忽略外部类,例如java.lang.String :
清单1.样本依赖项XML报告
<DependencyReport date="Sun Dec 03 22:30:21 EST 2006">
<FiltersApplied>
<Filter pattern="java|org"/>
<Filter pattern="net."/>
</FiltersApplied>
<Class name="com.acme.web.Widget">
<Dependency name="com.acme.resource.Configuration"/>
<Dependency name="com.acme.xml.Document"/>
</Class>
<Class name="com.acme.web.Account">
<Dependency name="com.acme.resource.Configuration"/>
<Dependency name="com.acme.xml.Document"/>
</Class>
</DependencyReport>
清单1显然是由应用程序生成的; 因此,第一级测试是验证应用程序实际上可以生成文档。 验证之后,您将至少要测试特定文档的三个方面:
结构体 内容 具体内容
您可以使用String比较仅通过JUnit处理前两个方面,如清单2所示:
清单2.用困难的方式验证XML
public class XMLReportTest extends TestCase {
private Filter[] getFilters(){
Filter[] fltrs = new Filter[2];
fltrs[0] = new RegexPackageFilter("java|org");
fltrs[1] = new SimplePackageFilter("net.");
return fltrs;
}
private Dependency[] getDependencies(){
Dependency[] deps = new Dependency[2];
deps[0] = new Dependency("com.acme.resource.Configuration");
deps[1] = new Dependency("com.acme.xml.Document");
return deps;
}
public void testToXML() {
Date now = new Date();
BatchDependencyXMLReport report =
new BatchDependencyXMLReport(now, this.getFilters());
report.addTargetAndDependencies(
"com.acme.web.Widget", this.getDependencies());
report.addTargetAndDependencies(
"com.acme.web.Account", this.getDependencies());
String valid = "<DependencyReport date=\"" + now.toString() + "\">"+
"<FiltersApplied><Filter pattern=\"java|org\" /><Filter pattern=\"net.\" />"+
"</FiltersApplied><Class name=\"com.acme.web.Widget\">" +
" <Dependency name=\"com.acme.resource.Configuration\" />"+
"<Dependency name=\"com.acme.xml.Document\" /></Class>"+
"<Class name=\"com.acme.web.Account\">"+
"<Dependency name=\"com.acme.resource.Configuration\" />"+
"<Dependency name=\"com.acme.xml.Document\" />"+
"</Class></DependencyReport>";
assertEquals("report didn't match xml", valid, report.toXML());
}
}
清单2中的测试有一些主要缺点-不仅限于硬编码的String比较。 首先,测试不完全可读。 其次,它非常脆弱。 如果XML文档的格式发生更改(包括添加空格),则最好粘贴文档的新副本,而不是尝试修复String本身。 最后,即使您可能并不在意测试的性质,您也必须应对Date方面的问题。
如果要确保文档中第二个Class元素的name值为com.acme.web.Account怎么办? 当然,您可以使用正则表达式或String搜索,但这会花费太多精力。 使用解析框架直接操作DOM会更有意义吗?
XMLUnit是JUnit扩展,但这并不一定意味着您不能在TestNG中使用它。 您可以将几乎任何框架都集成到TestNG中,只要它具有支持委托且不基于装饰器的API。
使用XMLUnit进行测试
当您感觉到自己在努力工作时,通常可以认为其他人已经找到了解决问题的简便方法。 在以编程方式验证XML文档时,想到的解决方案是XMLUnit。
XMLUnit是一个JUnit扩展框架,可帮助开发人员测试XML文档。 实际上,XMLUnit是一个名副其实的XML测试帽子戏法:您可以使用它来验证XML文档的结构,其内容,甚至文档的特定部分。
最简单的操作是使用XMLUnit在逻辑上将运行时XML文档与预定义的有效控制文件进行比较。 本质上,这是一个差异测试 :给定您知道正确的XML文档,应用程序在运行时是否会生成相同的东西? 这是一个相对简单的测试,但是您可以使用它来验证XML文档的结构和内容。 您也可以在XPath的帮助下验证特定内容。
根据经验,应尽可能避免继承测试用例。 许多JUnit扩展框架,包括XMLUnit,都提供了专门的测试用例,可以从中继承这些专用用例,以方便测试特定的体系结构。 但是,由于Java平台的单继承范式,从框架继承类的测试用例具有灵活性。 通常,这些相同的JUnit扩展框架提供了委托API,这使得可以轻松组合各种框架而无需采用严格的继承结构。
验证内容
您可以通过委派或继承来利用XMLUnit。 根据经验,我建议避免测试用例继承 。 另一方面,从XMLUnit的XMLTestCase继承确实提供了一些方便的断言方法(它们不是static ,因此不能像JUnit的TestCase断言一样被静态引用)。
无论选择如何使用XMLUnit,都必须初始化XMLUnit的解析器。 您可以通过System.setProperty调用或通过XMLUnit核心类上的一些方便的static方法来初始化它们。
使用各种必需的解析器正确初始化XMLUnit之后,就可以使用Diff类,这是用于逻辑比较两个XML文档的中心机制。 在清单3中,我使用了一些XMLUnit改进了testToXML测试 :
清单3.改进的testToXML测试
public class XMLReportTest extends TestCase {
protected void setUp() throws Exception {
XMLUnit.setControlParser(
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
XMLUnit.setTestParser(
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
XMLUnit.setSAXParserFactory(
"org.apache.xerces.jaxp.SAXParserFactoryImpl");
XMLUnit.setIgnoreWhitespace(true);
}
private Filter[] getFilters(){
Filter[] fltrs = new Filter[2];
fltrs[0] = new RegexPackageFilter("java|org");
fltrs[1] = new SimplePackageFilter("net.");
return fltrs;
}
private Dependency[] getDependencies(){
Dependency[] deps = new Dependency[2];
deps[0] = new Dependency("com.acme.resource.Configuration");
deps[1] = new Dependency("com.acme.xml.Document");
return deps;
}
public void testToXML() {
BatchDependencyXMLReport report =
new BatchDependencyXMLReport(new Date(1165203021718L),
this.getFilters());
report.addTargetAndDependencies(
"com.acme.web.Widget", this.getDependencies());
report.addTargetAndDependencies(
"com.acme.web.Account", this.getDependencies());
Diff diff = new Diff(new FileReader(
new File("./test/conf/report-control.xml")),
new StringReader(report.toXML()));
assertTrue("XML was not identical", diff.identical());
}
}
注意我的装置如何初始化XMLUnit的setControlParser , setTestParser和setSAXParserFactory方法。 您可以使用任何符合JAXP的解析器框架来获取这些值。 另请注意,我将true称为setIgnoreWhitespace ,这是一个救命稻草,相信我! 否则,当两个文档由于空白不一致而不同时,您会发现很多失败!
与Diff的比较
Diff类支持两种类型的比较: identical和similar 。 如果两个比较的文档在结构和值上完全相同(如果设置了该标志,则忽略空白),则它们被视为相同 ; 如果两个文档相同,则它们也将相似 。 相反,不一定是正确的。
例如,清单4显示了一个简单的XML代码段,该代码段在逻辑上类似于清单5中的XML。 但是,它们并不相同:
清单4.帐户XML代码段
<account>
<id>3A-00</id>
<name>acme</name>
</account>
清单5中的XML代码片段与清单4中的逻辑文档相同。但是,XMLUnit认为它们不是相同的,因为name和id元素已交换。
清单5.一个类似的XML代码片段
<account>
<name>acme</name>
<id>3A-00</id>
</account>
因此,我可以编写一个测试用例来验证XMLUnit的行为,如清单6所示:
清单6.验证相似和相同的测试
public void testIdenticalAndSimilar() throws Exception {
String controlXML = "<account><id>3A-00</id><name>acme</name></account>";
String testXML = "<account><name>acme</name><id>3A-00</id></account>";
Diff diff = new Diff(controlXML, testXML);
assertTrue(diff.similar());
assertFalse(diff.identical());
}
相似和相同的XML文档之间的区别是细微的。 但是,验证两者的能力可能会非常有帮助,例如在测试情况下,文件是由不同的应用程序或客户端生成的。
验证结构
除了验证内容之外,您有时还需要验证XML文档的结构。 在这种情况下,单个元素和属性的值无关紧要-这是您所关注的结构。
幸运的是,通过有效地忽略元素文本值和属性值,我可以重用清单3中定义的测试用例来验证文档的结构。 我通过在Diff类上调用overrideDifferenceListener()并将其提供给IgnoreTextAndAttributeValuesDifferenceListener ,该对象由XMLUnit提供。 修改后的测试如清单7所示:
清单7.验证没有属性值的XML结构
public void testToXMLFormatOnly() throws Exception{
BatchDependencyXMLReport report =
new BatchDependencyXMLReport(new Date(), this.getFilters());
report.addTargetAndDependencies(
"com.acme.web.Widget", this.getDependencies());
report.addTargetAndDependencies(
"com.acme.web.Account", this.getDependencies());
Diff diff = new Diff(new FileReader(
new File("./test/conf/report-control.xml")),
new StringReader(report.toXML()));
diff.overrideDifferenceListener(
new IgnoreTextAndAttributeValuesDifferenceListener());
assertTrue("XML was not similar", diff.similar());
}
使用IgnoreTextAndAttributeValuesDifferenceListener类时,必须断言两个文档是similar 且不 identical 。 如果您错误地调用identical的属性值,则将对其进行处理。
当然,DTD和XML模式有助于XML结构验证。 但是,有时文档没有引用它们-在这种情况下,结构验证可能会有所帮助。 同样,如果您需要忽略特定的值(例如Date ),则可以实现DifferenceListener接口(如IgnoreTextAndAttributeValuesDifferenceListener那样)并提供自定义实现。
具有XPath的XMLUnit
为了完成XML测试帽技巧,XMLUnit有助于使用XPath验证XML文档的特定部分。
例如,使用清单1中相同的文档格式,我想验证我的应用程序生成的第一个Class元素的name属性值是com.acme.web.Widget 。 为此,我必须创建一个XPath表达式以导航到确切的位置。 此外,XMLUnit的XMLTestCase提供了一个方便的assertXpathExists()方法,这意味着我现在必须扩展XMLTestCase 。
清单8.使用XPath验证精确的XML值
public void testToXMLFormatOnly() throws Exception{
BatchDependencyXMLReport report =
new BatchDependencyXMLReport(new Date(), this.getFilters());
report.addTargetAndDependencies(
"com.acme.web.Widget", this.getDependencies());
report.addTargetAndDependencies(
"com.acme.web.Account", this.getDependencies());
assertXpathExists("//Class[1][@name='com.acme.web.Widget']",
report.toXML());
}
如清单8所示,XMLUnit与XPath协作,提供了一种方便的机制来验证XML文档的精确方面,而不是进行大的差异测试。 请记住,要利用XMLUnit中的XPath,您的测试用例必须扩展XMLTestCase 。 熟悉XPath也有帮助!
XPath或XML路径语言是一种表达语言,用于基于树表示寻址XML文档的各个部分。 XPath允许您浏览XML文档,并有助于选择文档值。
为什么要努力工作?
XMLUnit是一个基于Java的开源工具,与使用String比较可以做的任何事情相比,它使测试XML文档变得更加容易和灵活。 使用XMLUnit进行差异测试的唯一可能缺点是,测试将依赖文件系统来加载控制文档。 在编写测试时,请考虑这种增加的依赖性。
尽管XMLUnit已有一段时间没有发布任何新更新,但其当前的功能集合足够强大,足以为测试带来巨大的成功-在这种情况下,它基本上是免费的!
翻译自: https://www.ibm.com/developerworks/java/library/j-cq121906/index.html
相关资源:xmlunit-1.5.jar