1. 概要

XMLUnit 2.x は、XMLコンテンツのテストと検証に役立つ強力なライブラリであり、そのXMLに何が含まれるべきかを正確に把握している場合に特に便利です。

そのため、主に単体テスト内でXMLUnitを使用して、有効なXML であること、特定の情報が含まれていること、または特定のスタイルのドキュメントに準拠していることを確認します。

さらに、XMLUnitを使用すると、私たちにとって重要な違いの種類と、比較XMLのどの部分と比較するスタイル参照のどの部分を制御できます。

XMLUnit1.xではなくXMLUnit2.xに焦点を当てているため、XMLUnitという単語を使用する場合は常に、厳密に2.xを指します。

最後に、アサーションにもHamcrestマッチャーを使用するので、慣れていない場合はHamcrestをブラッシュアップすることをお勧めします。

2. XMLUnitMavenのセットアップ

Mavenプロジェクトでライブラリを使用するには、pom.xmlに次の依存関係が必要です。

<dependency>
    <groupId>org.xmlunit</groupId>
    <artifactId>xmlunit-core</artifactId>
    <version>2.2.1</version>
</dependency>

xmlunit-core の最新バージョンは、このリンクをたどることで見つけることができます。 と:

<dependency>
    <groupId>org.xmlunit</groupId>
    <artifactId>xmlunit-matchers</artifactId>
    <version>2.2.1</version>
</dependency>

xmlunit-matchers の最新バージョンは、このリンクで入手できます。

3. XMLの比較

3.1. 簡単な違いの例

2つのXMLがあると仮定しましょう。 ドキュメント内のノードの内容とシーケンスが完全に同じである場合、これらは同一であると見なされるため、次のテストに合格します。

@Test
public void given2XMLS_whenIdentical_thenCorrect() {
    String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    String testXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    assertThat(testXml, CompareMatcher.isIdenticalTo(controlXml));
}

この次のテストは、2つのXMLが類似しているが、ノードが異なるシーケンスで発生するため、同一ではないため失敗します。

@Test
public void given2XMLSWithSimilarNodesButDifferentSequence_whenNotIdentical_thenCorrect() {
    String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
    assertThat(testXml, assertThat(testXml, not(isIdenticalTo(controlXml)));
}

3.2. 詳細な違いの例

上記の2つのXMLドキュメント間の差異は、差異エンジンによって検出されます。

デフォルトでは、効率上の理由から、最初の違いが見つかるとすぐに比較プロセスが停止します。

2つのXMLの違いをすべて取得するには、次のようにDiffクラスのインスタンスを使用します。

@Test
public void given2XMLS_whenGeneratesDifferences_thenCorrect(){
    String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
    Diff myDiff = DiffBuilder.compare(controlXml).withTest(testXml).build();
    
    Iterator<Difference> iter = myDiff.getDifferences().iterator();
    int size = 0;
    while (iter.hasNext()) {
        iter.next().toString();
        size++;
    }
    assertThat(size, greaterThan(1));
}

while ループで返された値を出力すると、結果は次のようになります。

Expected element tag name 'int' but was 'boolean' - 
  comparing <int...> at /struct[1]/int[1] to <boolean...> 
    at /struct[1]/boolean[1] (DIFFERENT)
Expected text value '3' but was 'false' - 
  comparing <int ...>3</int> at /struct[1]/int[1]/text()[1] to 
    <boolean ...>false</boolean> at /struct[1]/boolean[1]/text()[1] (DIFFERENT)
Expected element tag name 'boolean' but was 'int' - 
  comparing <boolean...> at /struct[1]/boolean[1] 
    to <int...> at /struct[1]/int[1] (DIFFERENT)
Expected text value 'false' but was '3' - 
  comparing <boolean ...>false</boolean> at /struct[1]/boolean[1]/text()[1] 
    to <int ...>3</int> at /struct[1]/int[1]/text()[1] (DIFFERENT)

各インスタンスは、コントロールノードとテストノードの間に見られる違いのタイプと、それらのノードの詳細(各ノードのXPathの場所を含む)の両方を記述します。

最初の差が見つかった後にDifferenceEngineを強制的に停止させ、それ以上の差の列挙に進まない場合は、ComparisonControllerを指定する必要があります。

@Test
public void given2XMLS_whenGeneratesOneDifference_thenCorrect(){
    String myControlXML = "<struct><int>3</int><boolean>false</boolean></struct>";
    String myTestXML = "<struct><boolean>false</boolean><int>3</int></struct>";
    
    Diff myDiff = DiffBuilder
      .compare(myControlXML)
      .withTest(myTestXML)
      .withComparisonController(ComparisonControllers.StopWhenDifferent)
       .build();
    
    Iterator<Difference> iter = myDiff.getDifferences().iterator();
    int size = 0;
    while (iter.hasNext()) {
        iter.next().toString();
        size++;
    }
    assertThat(size, equalTo(1));
}

違いのメッセージはもっと単純です:

Expected element tag name 'int' but was 'boolean' - 
  comparing <int...> at /struct[1]/int[1] 
    to <boolean...> at /struct[1]/boolean[1] (DIFFERENT)

4. 入力ソース

XMLUnit を使用すると、アプリケーションのニーズに便利なさまざまなソースからXMLデータを選択できます。 この場合、静的メソッドの配列とともにInputクラスを使用します。

プロジェクトルートにあるXMLファイルから入力を選択するには、次のようにします。

@Test
public void givenFileSource_whenAbleToInput_thenCorrect() {
    ClassLoader classLoader = getClass().getClassLoader();
    String testPath = classLoader.getResource("test.xml").getPath();
    String controlPath = classLoader.getResource("control.xml").getPath();
    
    assertThat(
      Input.fromFile(testPath), isSimilarTo(Input.fromFile(controlPath)));
}

XML文字列から入力ソースを選択するには、次のようにします。

@Test
public void givenStringSource_whenAbleToInput_thenCorrect() {
    String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    String testXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    
    assertThat(
      Input.fromString(testXml),isSimilarTo(Input.fromString(controlXml)));
}

ここで、入力としてストリームを使用しましょう。

@Test
public void givenStreamAsSource_whenAbleToInput_thenCorrect() {
    assertThat(Input.fromStream(XMLUnitTests.class
      .getResourceAsStream("/test.xml")),
        isSimilarTo(
          Input.fromStream(XMLUnitTests.class
            .getResourceAsStream("/control.xml"))));
}

Input.from(Object)を使用して、XMLUnitによって解決される有効なソースを渡すこともできます。

たとえば、次のファイルを渡すことができます。

@Test
public void givenFileSourceAsObject_whenAbleToInput_thenCorrect() {
    ClassLoader classLoader = getClass().getClassLoader();
    
    assertThat(
      Input.from(new File(classLoader.getResource("test.xml").getFile())), 
      isSimilarTo(Input.from(new File(classLoader.getResource("control.xml").getFile()))));
}

または文字列:

@Test
public void givenStringSourceAsObject_whenAbleToInput_thenCorrect() {
    assertThat(
      Input.from("<struct><int>3</int><boolean>false</boolean></struct>"),
      isSimilarTo(Input.from("<struct><int>3</int><boolean>false</boolean></struct>")));
}

またはストリーム:

@Test
public void givenStreamAsObject_whenAbleToInput_thenCorrect() {
    assertThat(
      Input.from(XMLUnitTest.class.getResourceAsStream("/test.xml")), 
      isSimilarTo(Input.from(XMLUnitTest.class.getResourceAsStream("/control.xml"))));
}

そしてそれらはすべて解決されます。

5. 特定のノードの比較

上記のセクション2では、 xmlunit-core ライブラリの機能を使用して同様のXMLを少しカスタマイズする必要があるため、同一のXMLのみを確認しました。

@Test
public void given2XMLS_whenSimilar_thenCorrect() {
    String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
    
    assertThat(testXml, isSimilarTo(controlXml));
}

XMLには同様のノードがあるため、上記のテストは合格するはずですが、失敗します。 これは、 XMLUnitが、ルートノードに対して同じ深さのコントロールノードとテストノードを比較するためです。

したがって、 isSimilarTo 条件は、isIdenticalTo条件よりもテストするのが少し面白くなります。 controlXmlのノード3 は、testXmlfalseと自動的に比較されます。失敗メッセージを出す:

java.lang.AssertionError: 
Expected: Expected element tag name 'int' but was 'boolean' - 
  comparing <int...> at /struct[1]/int[1] to <boolean...> at /struct[1]/boolean[1]:
<int>3</int>
   but: result was: 
<boolean>false</boolean>

ここで、XMLUnitのDefaultNodeMatcherクラスとElementSelectorクラスが役立ちます。

DefaultNodeMatcher クラスは、 controlXml、のノードをループして、現在のXMLノードと比較する testXml のXMLノードを決定するときに、比較段階でXMLUnitによって参照されます。 controlXmlで発生します。

その前に、DefaultNodeMatcherはすでにElementSelectorを参照して、ノードの照合方法を決定しています。

デフォルトの状態では、XMLUnitは深さ優先アプローチを使用してXMLをトラバースし、ドキュメントの順序に基づいてノードを照合するため、テストは失敗しました。 と一致します 。

合格するようにテストを微調整しましょう。

@Test
public void given2XMLS_whenSimilar_thenCorrect() {
    String controlXml = "<struct><int>3</int><boolean>false</boolean></struct>";
    String testXml = "<struct><boolean>false</boolean><int>3</int></struct>";
    
    assertThat(testXml, 
      isSimilarTo(controlXml).withNodeMatcher(
      new DefaultNodeMatcher(ElementSelectors.byName)));
}

この場合、 DefaultNodeMatcher に、XMLUnitがノードの比較を要求するときに、ノードを要素名で並べ替えて一致させる必要があることを伝えています。

最初に失敗した例は、ElementSelectors.DefaultDefaultNodeMatcherに渡すのと似ていました。

または、 xmlunit-matchers を使用するのではなく、xmlunit-coreDiffを使用することもできます。

@Test
public void given2XMLs_whenSimilarWithDiff_thenCorrect() throws Exception {
    String myControlXML = "<struct><int>3</int><boolean>false</boolean></struct>";
    String myTestXML = "<struct><boolean>false</boolean><int>3</int></struct>";
    Diff myDiffSimilar = DiffBuilder.compare(myControlXML).withTest(myTestXML)
      .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName))
      .checkForSimilar().build();
    
    assertFalse("XML similar " + myDiffSimilar.toString(),
      myDiffSimilar.hasDifferences());
}

6. カスタムDifferenceEvaluator

DifferenceEvaluator は、比較の結果を決定します。 その役割は、比較結果の重大度を決定することに限定されています。

これは、2つのXMLピースが同一類似、または異なるであるかどうかを決定するクラスです。

次のXML部分を検討してください。

<a>
    <b attr="abc">
    </b>
</a>

と:

<a>
    <b attr="xyz">
    </b>
</a>

デフォルトの状態では、 attr 属性の値が異なるため、技術的には異なると評価されます。 テストを見てみましょう:

@Test
public void given2XMLsWithDifferences_whenTestsDifferentWithoutDifferenceEvaluator_thenCorrect(){
    final String control = "<a><b attr=\"abc\"></b></a>";
    final String test = "<a><b attr=\"xyz\"></b></a>";
    Diff myDiff = DiffBuilder.compare(control).withTest(test)
      .checkForSimilar().build();
    assertFalse(myDiff.toString(), myDiff.hasDifferences());
}

失敗メッセージ:

java.lang.AssertionError: Expected attribute value 'abc' but was 'xyz' - 
  comparing <b attr="abc"...> at /a[1]/b[1]/@attr 
  to <b attr="xyz"...> at /a[1]/b[1]/@attr

属性をあまり気にしない場合は、DifferenceEvaluatorの動作を変更して無視することができます。 これを行うには、独自に作成します。

public class IgnoreAttributeDifferenceEvaluator implements DifferenceEvaluator {
    private String attributeName;
    public IgnoreAttributeDifferenceEvaluator(String attributeName) {
        this.attributeName = attributeName;
    }
    
    @Override
    public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
        if (outcome == ComparisonResult.EQUAL)
            return outcome;
        final Node controlNode = comparison.getControlDetails().getTarget();
        if (controlNode instanceof Attr) {
            Attr attr = (Attr) controlNode;
            if (attr.getName().equals(attributeName)) {
                return ComparisonResult.SIMILAR;
            }
        }
        return outcome;
    }
}

次に、最初に失敗したテストを書き直して、次のように独自のDifferenceEvaluatorインスタンスを提供します。

@Test
public void given2XMLsWithDifferences_whenTestsSimilarWithDifferenceEvaluator_thenCorrect() {
    final String control = "<a><b attr=\"abc\"></b></a>";
    final String test = "<a><b attr=\"xyz\"></b></a>";
    Diff myDiff = DiffBuilder.compare(control).withTest(test)
      .withDifferenceEvaluator(new IgnoreAttributeDifferenceEvaluator("attr"))
      .checkForSimilar().build();
    
    assertFalse(myDiff.toString(), myDiff.hasDifferences());
}

今回は通過します。

7. 検証

XMLUnitは、Validatorクラスを使用してXML検証を実行します。 forLanguage ファクトリメソッドを使用してインスタンスを作成し、検証で使用するスキーマを渡します。

スキーマはその場所につながるURIとして渡され、XMLUnitはLanguagesクラスでサポートするスキーマの場所を定数として抽象化します。

通常、次のようにValidatorクラスのインスタンスを作成します。

Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);

この手順の後、XMLに対して検証する独自のXSDファイルがある場合は、そのソースを指定してから、XMLファイルソースを使用してValidatorvalidateInstanceメソッドを呼び出します。

たとえば、students.xsdを見てください。

<?xml version = "1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name='class'>
        <xs:complexType>
            <xs:sequence>
                <xs:element name='student' type='StudentObject'
                   minOccurs='0' maxOccurs='unbounded' />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="StudentObject">
        <xs:sequence>
            <xs:element name="name" type="xs:string" />
            <xs:element name="age" type="xs:positiveInteger" />
        </xs:sequence>
        <xs:attribute name='id' type='xs:positiveInteger' />
    </xs:complexType>
</xs:schema>

そしてstudents.xml

<?xml version = "1.0"?>
<class>
    <student id="393">
        <name>Rajiv</name>
        <age>18</age>
    </student>
    <student id="493">
        <name>Candie</name>
        <age>19</age>
    </student>
</class>

次に、テストを実行してみましょう。

@Test
public void givenXml_whenValidatesAgainstXsd_thenCorrect() {
    Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);
    v.setSchemaSource(Input.fromStream(
      XMLUnitTests.class.getResourceAsStream("/students.xsd")).build());
    ValidationResult r = v.validateInstance(Input.fromStream(
      XMLUnitTests.class.getResourceAsStream("/students.xml")).build());
    Iterator<ValidationProblem> probs = r.getProblems().iterator();
    while (probs.hasNext()) {
        probs.next().toString();
    }
    assertTrue(r.isValid());
}

検証の結果は、 ValidationResult のインスタンスであり、ドキュメントが正常に検証されたかどうかを示すブールフラグが含まれています。

ValidationResult には、障害が発生した場合に備えて、IterableValidationProblemも含まれています。 と呼ばれるエラーのある新しいXMLを作成しましょう student_with_error.xml。 それ以外の 、開始タグはすべて

<?xml version = "1.0"?>
<class>
    <studet id="393">
        <name>Rajiv</name>
        <age>18</age>
    </student>
    <studet id="493">
        <name>Candie</name>
        <age>19</age>
    </student>
</class>

次に、それに対してこのテストを実行します。

@Test
public void givenXmlWithErrors_whenReturnsValidationProblems_thenCorrect() {
    Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);
    v.setSchemaSource(Input.fromStream(
       XMLUnitTests.class.getResourceAsStream("/students.xsd")).build());
    ValidationResult r = v.validateInstance(Input.fromStream(
      XMLUnitTests.class.getResourceAsStream("/students_with_error.xml")).build());
    Iterator<ValidationProblem> probs = r.getProblems().iterator();
    int count = 0;
    while (probs.hasNext()) {
        count++;
        probs.next().toString();
    }
    assertTrue(count > 0);
}

while ループでエラーを出力すると、次のようになります。

ValidationProblem { line=3, column=19, type=ERROR,message='cvc-complex-type.2.4.a: 
  Invalid content was found starting with element 'studet'. 
    One of '{student}' is expected.' }
ValidationProblem { line=6, column=4, type=ERROR, message='The element type "studet" 
  must be terminated by the matching end-tag "</studet>".' }
ValidationProblem { line=6, column=4, type=ERROR, message='The element type "studet" 
  must be terminated by the matching end-tag "</studet>".' }

8. XPath

XPath式がXMLの一部に対して評価されると、一致するノードを含むNodeListが作成されます。

Teachers.xmlというファイルに保存されている次のXMLについて考えてみます。

<teachers>
    <teacher department="science" id='309'>
        <subject>math</subject>
        <subject>physics</subject>
    </teacher>
    <teacher department="arts" id='310'>
        <subject>political education</subject>
        <subject>english</subject>
    </teacher>
</teachers>

XMLUnitは、以下に示すように、XPathに関連する多数のアサーションメソッドを提供します。

Teacher と呼ばれるすべてのノードを取得し、それらに対して個別にアサーションを実行できます。

@Test
public void givenXPath_whenAbleToRetrieveNodes_thenCorrect() {
    Iterable<Node> i = new JAXPXPathEngine()
      .selectNodes("//teacher", Input.fromFile(new File("teachers.xml")).build());
    assertNotNull(i);
    int count = 0;
    for (Iterator<Node> it = i.iterator(); it.hasNext();) {
        count++;
        Node node = it.next();
        assertEquals("teacher", node.getNodeName());
        
        NamedNodeMap map = node.getAttributes();
        assertEquals("department", map.item(0).getNodeName());
        assertEquals("id", map.item(1).getNodeName());
        assertEquals("teacher", node.getNodeName());
    }
    assertEquals(2, count);
}

子ノードの数、各ノードの名前、および各ノードの属性を検証する方法に注目してください。 ノードを取得すると、さらに多くのオプションを使用できます。

パスが存在することを確認するには、次のようにします。

@Test
public void givenXmlSource_whenAbleToValidateExistingXPath_thenCorrect() {
    assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//teachers"));
    assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//teacher"));
    assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//subject"));
    assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//@department"));
}

パスが存在しないことを確認するには、次のようにします。

@Test
public void givenXmlSource_whenFailsToValidateInExistentXPath_thenCorrect() {
    assertThat(Input.fromFile(new File("teachers.xml")), not(hasXPath("//sujet")));
}

XPathは、ドキュメントの大部分が既知の変更されていないコンテンツで構成されており、システムによって作成された変更コンテンツが少量である場合に特に便利です。

9. 結論

このチュートリアルでは、 XMLUnit 2.x の基本機能のほとんどと、それらを使用してアプリケーションでXMLドキュメントを検証する方法を紹介しました。

これらすべての例とコードスニペットの完全な実装は、 XMLUnit GitHubプロジェクトにあります。