BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル XMLをユニットテストする

XMLをユニットテストする

ブックマーク
ソフトウエアがXML出力を作成する場合には、多くの根拠があります。XML文書は異なるアプリケーション間のデータ交換のために使われますし、webアプリケーションはわずかなXMLスニペットを使って(X)HTML出力を作成したりAJAXリクエストに答えたりします。XMLが生成されるところには多くの利用ケースがあるので、その出力は他のアプリケーションの部分と同様にテストされるべきなのです。

生成されたXMLをテストするには幾つかの方法があります。その各々が分離して使用される際のフローを持っています。

例えば、下記のようなことができます。
  • 生成されたXMLがDTDに対して、またXMLスキーマや、或いは他の文法的代替物に対して正当であると確認することができます。残念なことに、このような文法はいつも文書中に存在するとは限りませんし、もしあったとしても、どのテストも出力の構造のみをテストするだけで、その内容はテストしません。
  • 生成された出力を期待される結果と比較することができます。残念なことに、同じツリー構造の情報を示す2つのXML文書からシリアライズされたとしても、極めて異なっている可能性があります。例えば、子ノードを持たない要素が空の要素になることもあれば、オープニングタグとクロージングタグを使えばシリアライズできる場合もあります。ホワイトスペースや文字コードには違いがあるかもしれません。
  • XPathクエリーを使い、生成された文書の部分的なコンテンツを抽出することができます。そしてそれらの価値を表明することができます。これは、テストする必要がある生成済みコンテンツの量が大きければ、退屈なものになるかもしれません。
  • プログラム的に文書を処理できます。-例えば、そのDOMオブジェクトモデルを使い-そして各ノードのコンテンツをアサ―トするといったことです。このように記述されたテストは非常に具体的で、出力構造が変化する際には大きな調整を強いられるかもしれません。
しかもいずれのタスクにとっても既存のAPIはしばしば不便なのです。例えばJAXP 1.3より前(すなわちJava SE 5より前)のJavaにおいて、文書はDTD或いは XMLスキーマに対してのみ正当であると立証できたのですが、一方でそれはバイトストリーム或いは文字ストリームからDOMドキュメントインスタンス或いはSAXイベントのインスタンスまで解析されたのです。

XMLUnit

XMLUnitはBSDライセンスの下で認可されたオープンソースプロジェクトです。XMLUnitは相関クラスの小さなライブラリを提供しますが、それは、前のセクションで概説された幾つかのXMLをテストする異なった方法のそれぞれを簡素化します。J/Nunitを使ってユニットテストを記述するのを簡略化するための特別なAPIが提供されますが、ライブラリそのものは、一切のテスティングフレームワークを使わずとも十分使用することができます。

XMLUnitのJavaと.NETのバージョンがありますが、Javaのバージョンはより十分に発展を遂げ、より多くの機能を提供しています。この記事はJavaのバージョンについてのみフォーカスし、全ての例はJavaを使っています。

XMLUnitは2001年Tim Bacon氏とJeff Martin氏により設立され、彼ら自身のプロジェクトのためのテストフレームワークとして開発されました。JavaのためのXMLUnitの最初の安定したリリースは2003年の3月に行われました。この1.0のリリースに続く4年間、XMLUnitは多くのオープンソース、また同じくらい多くのクローズドソースプロジェクトで使われてきました。しかしそのアクティブな展開は行き詰まってしまったのです。

同時に、XMLエコシステムに変更が加えられました。XMLUnit 1.0のバリデーションクラスはDTDに強力にフォーカスし、XMLスキーマは補足部としてのみサポートされました。同様に、XMLUnit1.0の一部であった単純化されたXPathエンジンは、一切のXML 名前空間をサポートしなくなりました。

2006年秋、XMLUnitの開発は再度着手され、XMLUnit1.1の最初のベータ版が2007年4月にリリースされ、最終リリースがもうすぐ出る予定です。既に、このリリースを超える更なる開発についての議論がXMLUnitのメーリングリスト上でなされています。

この記事では以降、XMLUnit 1.1を例に用いますが、多くは同XMLUnit 1.0にも当てはまるものです。

XMLUnitへの入力としてのXMLを提供する

XMLUnitのAPIは「幾つかのXML」を数種類の異なる形式を使って入力として受け取ります。殆どの場合、それらはInputStreams 、Readers、Strings、InputSources、或いは直ちに解析されたDOMドキュメントインスタンスとして提供が可能です。

XMLUnitはまたトランスフォームクラスを提供し、それは既存の入力(上記に記載した形式の1つを使って)へのXSLT変換に適用するために使うことが可能ですし、更なるテストにおいてこの変換の出力を使うことも可能です。

Transform tr = new Transform("",
new File("xml/example1.xsl"));
Document d = tr.getResultDocument();
assertEquals("example1", d.getDocumentElement().getTagName());

XMLの検証

XMLUnitはDTD 或いは W3C XMLスキーマに対してXML文書を検証することができます。XMLUnitの後のバージョンではJAXP 1.3と共に追加されたjavax.xml.validation パッケージを活用することになる予定です。それにより、RELAX NGやSchematron或いは他の文法についても同様に検証を提供できるかもしれません。

どの形式の検証についても、XMLUnitのValidatorクラスが使用されます。

W3C XMLスキーマに対する検証

DTDの検証はXMLUnitのデフォルトの設定ですので、スキーマ検証は、ValidatorのuseXMLSchema属性をtrueに設定することにより明確に可能でなければなりません。

XMLスキーマに対して検証を行うために、テスト下の文書はスキーマのURIを使ってXML名前空間を宣言しなければなりません。文書はまたXMLパーサーにスキーマ定義を見つける場所を教えるschemaLocation属性を提供します。


xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="file:///opt/schemas/example.com/order.xsd"/>

例1:名前空間宣言とschemaLocation属性を持つXML文書

schemaLocationが与えられなければ、XMLパーサーは名前空間URIをURLとして使用し、そこからスキーマ定義を読み込もうとします。

残念なことに、schemaLocation属性を提供することも名前空間URIとして有効なURLを使うことも実現可能ではないというケースがしばしばあります。生成されたXML出力が異なるマシン(カスタマーのサイトで、など)で処理されるかもしれないので、ローカルファイルリファレンスは作用しませんし、このマシンがネットワークアクセスを持たないのであれば、いかなるパブリックhttp URLもまた作用しないでしょう。

幸いにもJAXP1.2(すなわちJava1.4)は隠れ機能を提供しており、スキーマロケーションをプログラムで提供可能にします。スキーマのロケーションはファイル或いはURLまた、かなりの量のバイトとして特定される可能性があります。

String example = ""
+ ""
+ "";
Validator v = new Validator(example);
v.useXMLSchema(true);
v.setJAXP12SchemaSource(new File("xml/example3.xsd"));
assertTrue(v.toString(), v.isValid());

例2:W3C XMLスキーマに対してXML文書を検証し、スキーマロケーションをプログラムで提供する

XMLUnitは現在、XMLスキーマインスタンスドキュメントの検証をサポートするのみですが、スキーマ定義そのものが、有効なXMLスキーマであることを検証することはできないのです。将来のバージョンで大いにサポートを拡張するという計画があります。

DTDに対して検証する

XMLUnitは多くの異なるシナリオの中でDTDバリデーションをサポートします。その最も基本的なケースでは、テスト中の文書がSYSTEM識別子を提供するドキュメントタイプ宣言を含んでいます。


"file:///opt/schemas/example.com/order.xsd" >

例3:SYSTEM識別子とPUBLIC識別子を持つDOCTYPE宣言を伴うXML文書

このケースでは、パーサーは与えられた識別子を使って文書の場所を決めます。

XMLスキーマセクション内で概説されているのと同様の理由で、これは望ましいことではないかもしれないので、XMLUnitはユーザー自身のSYSTEM識別子を提供することを可能にします。もしそうすれば、既存のSYSTEM識別子をオーバーライドすることもあるでしょう。SYSTE識別子を特定することにより、いかなる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());

例4:DTDに対してXML文書を検証する、ロケーションをプログラムで提供する

別の方法として、DTDのためのロケーションを提供するSAX EntityResolverを指定することができます。これは、例えばApacheのXML Resolverライブラリを使ってOASIS Catalogへの問題解決を引き延ばすために利用することが可能です。

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"/>

例5:DTDロケーションを解決するためにOASISカタログを利用する

幾つかのXMLを比較する

XMLUnitが2つのXMLを比較すると、結果は下記の3つの状態のうち、1つになります。

  1. 2つのXMLは全く同じである
  2. 2つのXMLは類似している
  3. 2つのXMLは異なっている

XMLUnitは見つけた相違点のそれぞれを、復元可能か否か分類します(下記参照)。XMLは相違点が全く見つからなかった場合のみ等しいことになります。全ての相違点が復元可能であると判明した場合、文書は類似しているとみなされ、復元可能でない場合は、文書は異なっているとみなされます。

デフォルトでは、XMLUnitはわずか数種類の相違点を復元可能とみなすのみです。例えば、2つの文書が同じ名前空間に異なるプレフィックスを使っているとすると、それらの文書は同一ではなく、類似しているとみなされます。検出済みの相違点のフルリストはXMLUnitのユーザーガイド(source)に掲載されています。驚くべきことの1つに、XMLUnitは2つの文書が同じ要素を異なる順序で含んでいる場合、それらを類似しているとみなすことがあります。

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);

例6:2つのXMLを比較する

例6の最終行は、XMLAssertクラスにより提供された便利なメソッドであるassertXMLEqualを示しています。XMLの比較のためには幾つかのオーバーロードされたメソッドがあり、またその他のXMLテストシナリオはXMLUnitによりサポートされており、XMLUnitとJUnit 3.xかそれ以上のコンビネーションのためのAPIを単純化します。注意すべきことは、”assertXMLEqual”とは少々、誤ったネーミングであり、類似性のためのテストを提供する方法であって、等しいことをテストする方法ではない、ということなのです。2つのXMLが類似しているが同等でない場合、assertXMLEqualは誤る可能性があるかもしれません。

XMLUnitは比較結果をよりコントロールする幾つかの拡張ポイントを提供します。

DifferenceListener

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());

例7a:2つの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());

例7b:2つのXMLを比較し、コメント中の相違を無視する

ElementQualifier

XMLUnitが要素の順序を重要とはみなさないことを考えれば、既知ノードのどの子供が互いに比較される必要があるのか、常に明らかなわけではありません。デフォルトでは、XMLUnitは同じタグネームをもつ要素を互いに比較しようとしますが、時には、それが望ましからぬ結果を導いてしまうこともあります。


text
some other text



some other text
text

例8:エレメントタグネームに一部不備がある場合

上記の例では、要素であるテキストコンテンツは、正しい要素を選ぶためにそれらのタグネームに加えて使用される必要があります。これは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());

例9:ElementNameAndTextQualifierを使用する

ElementNameAndTextQualifierは、XMLUnitの配布物の一部であるElementQualifierインタフェースの幾つかの実装方法のうちの1つです。その上、匹敵するノードを識別するロジックが非常に具体的であるならば、自分自身の実装を提供することもできるのです。

DetailedDiff

例は、今までのところ、2つのXMLが同一であるかどうかについてのみ有効でした。2つの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());

例10:2つのXMLの全ての相違点を見つける

DetailedDiffはDiffのサブクラスの1つですので、2つの文書を類似しているものまたは異なっているものに分類する際に使用することが可能です。DetailedDiffとは異なり、Diffは修復不能な相違点に遭遇するやいなや、比較プロセスを中止します。したがって、全ての相違を見つける必要がなければ、パフォーマンス向上のためにDiffを使用すべきです。

DiffもDetailedDiffも2つのXMLの間の相違をオンデマンドで算出し、結果を格納します。このことは、もし異なるセットのオプションを使って比較を繰り返したい場合には、新規のDiffインスタンスを作成する必要があることを意味します。

その他の設定のオプション

殆どのXMLUnitの設定は、静的メソッドのXMLUnitクラスを使って行います。値が明示的にリセットされない限り、デフォルト値をどのように変更しても適合するでしょう。もしユニットテストケースのデフォルトの設定を変更するならば、各テストの後でそれらをリセットすることが適正な実践です(例えば、JUnit3.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());

例11:エレメントコンテンツホワイトスペース

上記例では2つのXMLは異なっているとみなされるでしょう。それは、1つ目のエレメントはテキストがネストしており(改行の文字)2つ目はそうでないからです。

XMLUnitのignoreWhitespaceプロパティをtrueに設定すれば、この相違を回避し、2つの文書は同一であるとみなされるでしょう。

その他のオプションとしては、コメントを無視するものや、CDATAセクションや「通常の」ネストしたテキストを1種のコンテンツとして処理するものを含んでいます。すなわち、下記の例はどちらの表現もパスすることになります。

String expected = ""; 
String actual = "";

Diff d = new Diff(expected, actual);
assertFalse(d.similar());

XMLUnit.setIgnoreDiffBetweenTextAndCDATA(true);
d = new Diff(expected, actual)
assertTrue(d.identical());

例12:CDATAセクションと「通常」のテキストを比較する

XPath Tests

従来のXMLUnitは自身のXSLTベースのXPathエンジンを使っていました。XMLUnit1.1は、現在ではJAXP 1.3のjavax.xml.xpathが実行時に使用可能であればこれを支持し、そうでなければ内部のものに後退する予定です。

バックエンドでどのXPathエンジンが使われているかに関係なく、XMLUnitは、XPath表現をXMLのある部分へ適用した結果をDOM NodeList或いは文字列として取得することをサポートします。通常は、1つの結果のみを望むのであれば、後者の形式の方がより適切です。そしてこの結果は属性の値であるかもしくはネストされた要素テキストです。

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());

例13:XPathクエリーをテストする

特に文書が同時に幾つもの名前空間を含んでいる場合には、XMLUnit1.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());

例14:ネームスペースド文書上のXPathクエリをテストする

NamespaceContextを使用する際は、次のことに注意することが重要です。それは、ネームスペースのURLのみが意味をなすのであり、プレフィックスはそうでない、ということです。NamespaceContextsで提供されるプレフィックスはXPathセレクターに適用するのであり、文書に対してではありません。文書内では、プレフィックスは完全に無視されるのです。

DOMツリーにおけるプログラムのテスト

時折、生成されたXMLがあらかじめ定義された結果と比較することが非常に困難であり、個別にテストしなければならないノードがあまりにも多くあるために、XPathを使って各ノードをテストするのは非常に複雑になってしまうことがあります。

このような状況のためにXMLUnitは生成されたXMLの各ノードを簡潔なインタフェースを使ってプログラムでテストできる、非常に強力なテスト方法を提供します。

下記の例では、生成されたXMLは全てのアイテム要素のid属性において1つのGUID(8つの16進数で表現された)を含むと仮定されます。テストは属性の値が期待されたフォーマットにマッチすることを検証し、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);
}
}

例15:XML文書の正当性をNodeTesterを使って検証する

ひとまとめにする

XMLを生成するどのソフトウエアでも、出力機能やその他の機能についてテストができなければなりません。

XMLをテストするには、幾つかの異なる方法が選択可能ですし、しばしば1つ以上の方法の組み合わせの方がベストな結果をもたらします。単純なケースでは、生成された出力を期待される出力と比較することで十分かもしれませんが、より複雑なケースでは、出力構造の正式な検証と、XPathクエリ(少量の出力の場合)を伴うコンテンツのテスト若しくはプログラムによるテストを組み合わせるべきです。

Javaの中でXMLを処理するためのAPIは、しばしば使いづらいものですが、XMLUnitは本記事内に概説された全てのテスト方法について簡素化したAPIを提供しています。

本記事は、XMLUnitの全ての側面をカバーしたものではありません。例えば、XML に不備があるHTML文書については、特別なDocumentBuilder や SAXParserクラスの実装によるサポートが存在します。幾つかの例に示されているXMLAssertクラスに加え、JUnitの TestCaseを拡張し、XMLAssertに似ているメソッドを提供するXMLTestCaseというクラスもあります。

XMLUnitについては、プロジェクトのwebサイト(英語)とユーザーガイド(英語)で更に学ぶことができます。

Stefan Bodewig氏(source)はドイツ、エッセンにあるWebOne Informatik GmbHの開発責任者です。彼はそこでMicrosoft .NETプラットフォームをベースとしたアプリケーションのアーキテクチャと開発に対する責任を負っています。Stefan氏はまた、XMLUnit とApache Antを含む幾つかのオープンソースプロジェクトの寄稿者でもあります。

原文はこちらです:http://www.infoq.com/articles/xml-unit-test
(このArticleは2007年6月11日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT