单元测试XML

在许多情况下,软件都会创建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时,结果可能是以下三种状态之一:

  1. 这两个XML完全相同
  2. 这两个XML相似
  3. 这两段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。

翻译自: https://www.infoq.com/articles/xml-unit-test/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值