# 追求代碼質量: 探究 XMLUnit
_一種用于測試 XML 文檔的 JUnit 擴展框架_
Java? 開發人員一般都很善于解決問題,所以由 Java 開發人員提出更容易的方法用以驗證 XML 文檔是很自然的事。本月,Andrew 將向您介紹 XMLUnit,一個能滿足您所有的 XML 驗證需求的 JUnit 擴展框架。
在軟件開發周期中,需要不時地驗證 XML 文檔的結構或內容。不管構建的是何種應用程序,測試 XML 文檔都具有一定的挑戰性,尤其是在沒有相關工具的情況下就更是如此。
本月,我將首先向您說明為何_不能_ 使用 `String` 比較來驗證 XML 文檔的結構和內容。之后,我會介紹 XMLUnit,一個由 Java 開發人員創建并可服務于 Java 開發人員的 XML 驗證工具,向您展示如何使用它來驗證 XML 文檔。
## 古典的 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 很明顯是由應用程序生成的;因而,第一層測試就是驗證應用程序是否真能生成一個文檔。一旦驗證了這一點,就可以繼續測試指定文檔的其他三個方面:
* 結構
* 內容
* 指定內容
可以通過單獨使用 JUnit 利用 `String` 比較處理上述前兩個方面,如清單 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 能否用于 TestNG?
XMLUnit 是一個 JUnit 擴展,但這并不意味著不能在 TestNG 使用它。只要它具有 API 而且此 API 支持委托同時不基于修飾器,那么您可以將幾乎任何框架整合進 TestNG。
## 用 XMLUnit 進行測試
當您感覺自己為完成一項任務而努力過了頭,您就可以想想解決此問題是否還有更容易的捷徑可尋。如果所要解決的問題涉及的是編程式地驗證 XML 文檔,那么所應想到的解決方案就是 XMLUnit。
XMLUnit 是一種 JUnit 擴展框架,有助于開發人員測試 XML 文檔。實際上,XMLUnit 是一種真正的 XML 測試的“多面手”:可以使用它來驗證 XML 文檔的結構、內容甚至該文檔的指定部分。
最簡單的做法是使用 XMLUnit 在邏輯上對比運行時 XML 文檔和預定義的有效控制文件。本質上講,這就是一種_差異測試_:假定一個 XML 文檔是正確的,那么此應用程序在運行時是否會生成同樣的東西?它是相對簡單的一種測試,但也可以使用它來驗證 XML 文檔的結構和內容。也可以通過 XPath 的一點幫助來驗證特定內容。
## 委托而非繼承
首要原則是盡量避免測試用例繼承。許多 JUnit 擴展框架,包括 XMLUnit,都提供可以通過繼承得到的專門的測試用例來協助測試某一特定的架構。從框架繼承來的測試用例都缺乏靈活性,這是 Java 平臺的單一繼承的范型所致。更多的時候,這些相同的 JUnit 擴展框架提供一個委托 API,此 API 可以更易于組合不同的框架,而無需采用嚴格的繼承結構。
## 驗證內容
可以通過委托或繼承的方式使用 XMLUnit。作為最佳策略,我建議[避免測試用例繼承](#sidebar2)。另一方面,從 XMLUnit 的 `XMLTestCase` 繼承確實可以提供一些方便的聲明方法(這些方法不是`靜態` 的,因而也就不能像 JUnit的 `TestCase` 聲明一樣被靜態引用)。
不管您如何選擇使用 XMLUnit,都必須實例化 XMLUnit 的解析器。您可以通過 `System.setProperty` 調用實例化它們,也可以通過 `XMLUnit` 核心類上的一些方便的 `static` 方法對它們進行實例化。
一旦用所需要的不同的解析器實例化 XMLUnit 之后,就可以使用 `Diff` 類,這是從邏輯上對比兩個 XML 文檔所需的中心機制。在清單 3 中,我利用 XMLUnit 對 [>testToXML test](#listing2) 做了一些改進:
##### 清單 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`。如果所比較的文檔在結構和值(如果設置了標志就忽略空白)方面都完全相同,那么它們就被認為是 _identical_;如果兩個文檔是完全相同的,那么它們也就很自然的是 _similar_ 的。反之,卻不一定。
例如,清單 4 是與清單 5 相似的一個簡單的 XML 代碼片段,但二者并不相同:
##### 清單 4\. 一個帳號 XML 片段
```
<account>
<id>3A-00</id>
<name>acme</name>
</account>
```
清單 5 中的 XML 片段與清單 4 中所示的 XML 片段有相同的邏輯文檔。但 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()` 并為它添加由 XMLUnit 提供的 `IgnoreTextAndAttributeValuesDifferenceListener`。修改后的測試如清單 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` 所做的一樣)并提供一個定制實現。
## XMLUnit 和 XPath
為實現 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 是什么?
XPath 或 XML Path Language 是一種表達式語言,用來基于樹表示定位 XML 文檔的各部分。XPath 允許您導航 XML 文檔并可以幫您選擇文檔值。
## 為何要舍近求遠呢?
XMLUnit 是一種基于 Java 的開放源碼工具,它使測試 XML 文檔更為簡單和靈活,而這是使用 `String` 比較所達不到的。使用 XMLUnit 進行差異測試所存在的惟一缺點是測試會依賴于文件系統來加載控制文檔。在編寫測試時,請務必考慮這一附加的依賴性。
雖然 XMLUnit 已經有段時間沒有發布任何更新了,但它當前的特性集已經足夠健壯來應對各種測試沖擊,并且它用在這種情況下基本上是免費的!
- 追求代碼質量
- 追求代碼質量: 對 Ajax 應用程序進行單元測試
- 追求代碼質量: 使用 TestNG-Abbot 實現自動化 GUI 測試
- 追求代碼質量: 用 AOP 進行防御性編程
- 追求代碼質量: 探究 XMLUnit
- 追求代碼質量: 用 JUnitPerf 進行性能測試
- 追求代碼質量: 通過測試分類實現敏捷構建
- 追求代碼質量: 可重復的系統測試
- 追求代碼質量: JUnit 4 與 TestNG 的對比
- 追求代碼質量: 馴服復雜的冗長代碼
- 追求代碼質量: 用代碼度量進行重構
- 追求代碼質量: 軟件架構的代碼質量
- 讓開發自動化: 除掉構建腳本中的氣味
- 追逐代碼質量: 決心采用 FIT
- 追求代碼質量: 不要被覆蓋報告所迷惑