Pengenalan Kepada XMLUnit 2.x

1. Gambaran keseluruhan

XMLUnit 2.x adalah pustaka hebat yang membantu kami menguji dan mengesahkan kandungan XML, dan sangat berguna apabila kami mengetahui dengan tepat apa yang harus mengandungi XML.

Oleh itu, kami akan menggunakan XMLUnit dalam ujian unit untuk mengesahkan bahawa apa yang kami miliki adalah XML yang sah , bahawa ia mengandungi maklumat tertentu atau sesuai dengan dokumen gaya tertentu.

Selain itu, dengan XMLUnit, kami dapat mengawal perbezaan apa yang penting bagi kami dan bahagian rujukan gaya mana yang hendak dibandingkan dengan bahagian XML perbandingan anda.

Oleh kerana kita memfokuskan pada XMLUnit 2.x dan bukan XMLUnit 1.x, setiap kali kita menggunakan perkataan XMLUnit, kita dengan tegas merujuk pada 2.x.

Akhirnya, kami juga akan menggunakan penjodoh Hamcrest untuk penegasan, jadi ada baiknya anda menggunakan Hamcrest sekiranya anda tidak biasa dengannya.

2. Persediaan XMLUnit Maven

Untuk menggunakan perpustakaan dalam projek maven kami, kami perlu mempunyai pergantungan berikut dalam pom.xml :

 org.xmlunit xmlunit-core 2.2.1 

Versi terbaru xmlunit-core boleh didapati dengan mengikuti pautan ini. Dan:

 org.xmlunit xmlunit-matchers 2.2.1 

Versi terbaru xmlunit-matchers terdapat di pautan ini.

3. Membandingkan XML

3.1. Contoh Perbezaan Mudah

Anggaplah kita mempunyai dua keping XML. Mereka dianggap sama ketika kandungan dan urutan simpul dalam dokumen sama persis, jadi ujian berikut akan lulus:

@Test public void given2XMLS_whenIdentical_thenCorrect() { String controlXml = "3false"; String testXml = "3false"; assertThat(testXml, CompareMatcher.isIdenticalTo(controlXml)); }

Ujian seterusnya gagal kerana kedua-dua kepingan XML serupa tetapi tidak sama kerana nod mereka berlaku dalam urutan yang berbeza :

@Test public void given2XMLSWithSimilarNodesButDifferentSequence_whenNotIdentical_thenCorrect() { String controlXml = "3false"; String testXml = "false3"; assertThat(testXml, assertThat(testXml, not(isIdenticalTo(controlXml))); }

3.2. Contoh Perbezaan Terperinci

Perbezaan antara dua dokumen XML di atas dikesan oleh Difference Engine .

Secara lalai dan atas sebab kecekapan, ia menghentikan proses perbandingan sebaik sahaja perbezaan pertama dijumpai.

Untuk mendapatkan semua perbezaan antara dua kepingan XML, kami menggunakan contoh kelas Diff seperti:

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

Sekiranya kita mencetak nilai yang dikembalikan dalam loop sementara , hasilnya adalah seperti di bawah:

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

Setiap contoh menerangkan kedua-dua jenis perbezaan yang terdapat antara nod kawalan dan node ujian dan perincian nod tersebut (termasuk lokasi XPath setiap nod).

Sekiranya kita mahu memaksa Difference Engine berhenti setelah perbezaan pertama dijumpai dan tidak meneruskan penghitungan perbezaan selanjutnya - kita perlu membekalkan ComparisonController :

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

Mesej perbezaan lebih mudah:

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

4. Sumber Input

Dengan XMLUnit , kita dapat memilih data XML dari pelbagai sumber yang mungkin sesuai untuk keperluan aplikasi kita. Dalam kes ini, kami menggunakan kelas Input dengan pelbagai kaedah statiknya.

Untuk memilih input dari fail XML yang terletak di root projek, kami melakukan perkara berikut:

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

Untuk memilih sumber input dari rentetan XML, seperti:

@Test public void givenStringSource_whenAbleToInput_thenCorrect() { String controlXml = "3false"; String testXml = "3false"; assertThat( Input.fromString(testXml),isSimilarTo(Input.fromString(controlXml))); }

Sekarang mari kita gunakan aliran sebagai input:

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

Kami juga dapat menggunakan Input.from (Object) di mana kami menyampaikan sumber yang sah untuk diselesaikan oleh XMLUnit.

Sebagai contoh, kita boleh menghantar fail dalam:

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

Atau Rentetan:

@Test public void givenStringSourceAsObject_whenAbleToInput_thenCorrect() { assertThat( Input.from("3false"), isSimilarTo(Input.from("3false"))); }

Atau Aliran:

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

dan semuanya akan diselesaikan.

5. Membandingkan Nod Spesifik

Pada bahagian 2 di atas, kami hanya melihat XML yang sama kerana XML yang serupa memerlukan sedikit penyesuaian menggunakan ciri dari perpustakaan inti xmlunit :

@Test public void given2XMLS_whenSimilar_thenCorrect() { String controlXml = "3false"; String testXml = "false3"; assertThat(testXml, isSimilarTo(controlXml)); }

Ujian di atas harus lulus kerana XML mempunyai nod yang serupa, namun ia gagal. Ini kerana XMLUnit membandingkan nod kawalan dan ujian pada kedalaman yang sama berbanding dengan nod akar .

So an isSimilarTo condition is a little bit more interesting to test than an isIdenticalTo condition. The node 3 in controlXml will be compared with false in testXml, automatically giving failure message:

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

This is where the DefaultNodeMatcher and ElementSelector classes of XMLUnit come in handy

The DefaultNodeMatcher class is consulted by XMLUnit at comparison stage as it loops over nodes of controlXml, to determine which XML node from testXml to compare with the current XML node it encounters in controlXml.

Before that, DefaultNodeMatcher will have already consulted ElementSelector to decide how to match nodes.

Our test has failed because in the default state, XMLUnit will use a depth-first approach to traversing the XMLs and based on document order to match nodes, hence is matched with .

Let's tweak our test so that it passes:

@Test public void given2XMLS_whenSimilar_thenCorrect() { String controlXml = "3false"; String testXml = "false3"; assertThat(testXml, isSimilarTo(controlXml).withNodeMatcher( new DefaultNodeMatcher(ElementSelectors.byName))); }

In this case, we are telling DefaultNodeMatcher that when XMLUnit asks for a node to compare, you should have sorted and matched the nodes by their element names already.

The initial failed example was similar to passing ElementSelectors.Default to DefaultNodeMatcher.

Alternatively, we could have used a Diff from xmlunit-core rather than using xmlunit-matchers:

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

6. Custom DifferenceEvaluator

A DifferenceEvaluator makes determinations of the outcome of a comparison. Its role is restricted to determining the severity of a comparison's outcome.

It's the class that decides whether two XML pieces are identical, similar or different.

Consider the following XML pieces:

and:

In the default state, they are technically evaluated as different because their attr attributes have different values. Let's take a look at a test:

@Test public void given2XMLsWithDifferences_whenTestsDifferentWithoutDifferenceEvaluator_thenCorrect(){ final String control = ""; final String test = ""; Diff myDiff = DiffBuilder.compare(control).withTest(test) .checkForSimilar().build(); assertFalse(myDiff.toString(), myDiff.hasDifferences()); }

Failure message:

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

If we don't really care about the attribute, we can change the behavior of DifferenceEvaluator to ignore it. We do this by creating our own:

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

We then rewrite our initial failed test and supply our own DifferenceEvaluator instance, like so:

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

This time it passes.

7. Validation

XMLUnit performs XML validation using the Validator class. You create an instance of it using the forLanguage factory method while passing in the schema to use in validation.

The schema is passed in as a URI leading to its location, XMLUnit abstracts the schema locations it supports in the Languages class as constants.

We typically create an instance of Validator class like so:

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

After this step, if we have our own XSD file to validate against our XML, we simply specify its source and then call Validator‘s validateInstance method with our XML file source.

Take for example our students.xsd:

And students.xml:

 Rajiv 18 Candie 19 

Let's then run a test:

@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 probs = r.getProblems().iterator(); while (probs.hasNext()) { probs.next().toString(); } assertTrue(r.isValid()); }

The result of the validation is an instance of ValidationResult which contains a boolean flag indicating whether the document has been validated successfully.

The ValidationResult also contains an Iterable with ValidationProblems in case there is a failure. Let's create a new XML with errors called students_with_error.xml. Instead of , our starting tags are all :

 Rajiv 18 Candie 19 

Then run this test against it:

@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 probs = r.getProblems().iterator(); int count = 0; while (probs.hasNext()) { count++; probs.next().toString(); } assertTrue(count > 0); }

If we were to print the errors in the while loop, they would look like:

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 ""." } ValidationProblem { line=6, column=4, type=ERROR, message="The element type "studet" must be terminated by the matching end-tag ""." }

8. XPath

When an XPath expression is evaluated against a piece of XML a NodeList is created that contains the matching Nodes.

Consider this piece of XML saved in a file called teachers.xml:

 math physics political education english 

XMLUnit offers a number of XPath related assertion methods, as demonstrated below.

We can retrieve all the nodes called teacher and perform assertions on them individually:

@Test public void givenXPath_whenAbleToRetrieveNodes_thenCorrect() { Iterable i = new JAXPXPathEngine() .selectNodes("//teacher", Input.fromFile(new File("teachers.xml")).build()); assertNotNull(i); int count = 0; for (Iterator 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); }

Notice how we validate the number of child nodes, the name of each node and the attributes in each node. Many more options are available after retrieving the Node.

To verify that a path exists, we can do the following:

@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")); }

To verify that a path does not exist, this is what we can do:

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

XPaths are especially useful where a document is made up largely of known, unchanging content with only a small amount of changing content created by the system.

9. Conclusion

In this tutorial, we have introduced most of the basic features of XMLUnit 2.x and how to use them to validate XML documents in our applications.

The full implementation of all these examples and code snippets can be found in the XMLUnit GitHub project.