Pengantar XMLUnit 2.x

1. Ikhtisar

XMLUnit 2.x adalah pustaka yang hebat yang membantu kita menguji dan memverifikasi konten XML, dan sangat berguna ketika kita tahu persis apa yang harus dimuat dalam XML itu.

Jadi kami terutama akan menggunakan XMLUnit di dalam pengujian unit untuk memverifikasi bahwa apa yang kami miliki adalah XML yang valid , yang berisi informasi tertentu atau sesuai dengan dokumen gaya tertentu.

Selain itu, dengan XMLUnit, kami memiliki kendali atas jenis perbedaan apa yang penting bagi kami dan bagian mana dari referensi gaya yang akan dibandingkan dengan bagian mana dari XML perbandingan Anda.

Karena kami berfokus pada XMLUnit 2.x dan bukan XMLUnit 1.x, setiap kali kami menggunakan kata XMLUnit, kami secara ketat mengacu pada 2.x.

Terakhir, kami juga akan menggunakan pencocok Hamcrest untuk pernyataan, jadi sebaiknya pelajari Hamcrest jika Anda tidak terbiasa dengannya.

2. Penyiapan XMLUnit Maven

Untuk menggunakan pustaka dalam proyek maven kita, kita perlu memiliki dependensi berikut di pom.xml :

 org.xmlunit xmlunit-core 2.2.1 

Versi terbaru xmlunit-core dapat ditemukan dengan mengikuti tautan ini. Dan:

 org.xmlunit xmlunit-matchers 2.2.1 

Versi terbaru xmlunit-matchers tersedia di tautan ini.

3. Membandingkan XML

3.1. Contoh Perbedaan Sederhana

Mari kita asumsikan kita memiliki dua buah XML. Mereka dianggap identik jika konten dan urutan node dalam dokumen sama persis, sehingga tes berikut akan lulus:

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

Tes berikutnya ini gagal karena dua buah XML serupa tetapi tidak identik karena node mereka muncul dalam urutan yang berbeda :

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

3.2. Contoh Perbedaan Detil

Perbedaan antara dua dokumen XML di atas dideteksi oleh Difference Engine .

Secara default dan untuk alasan efisiensi, ini menghentikan proses perbandingan segera setelah perbedaan pertama ditemukan.

Untuk mendapatkan semua perbedaan antara dua bagian XML, kami menggunakan turunan dari 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)); }

Jika kita mencetak nilai yang dikembalikan pada loop sementara , hasilnya seperti di bawah ini:

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 menjelaskan jenis perbedaan yang ditemukan antara node kontrol dan node pengujian dan detail dari node tersebut (termasuk lokasi XPath dari setiap node).

Jika kita ingin memaksa Difference Engine berhenti setelah perbedaan pertama ditemukan dan tidak melanjutkan untuk menghitung perbedaan lebih lanjut - kita perlu menyediakan 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)); }

Perbedaan pesannya lebih sederhana:

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 berbagai sumber yang mungkin sesuai untuk kebutuhan aplikasi kita. Dalam hal ini, kami menggunakan kelas Input dengan array metode statisnya.

Untuk memilih input dari file XML yang terletak di root proyek, kami melakukan hal 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 string XML, seperti ini:

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

Kita juga bisa menggunakan Input.from (Object) di mana kita meneruskan sumber yang valid untuk diselesaikan oleh XMLUnit.

Misalnya, kita dapat mengirimkan file di:

@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 String:

@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 terselesaikan.

5. Membandingkan Node Spesifik

Di bagian 2 di atas, kita hanya melihat XML yang identik karena XML yang serupa memerlukan sedikit penyesuaian menggunakan fitur dari pustaka xmlunit-core :

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

Tes di atas harus lulus karena XML memiliki node yang serupa, namun gagal. Ini karena XMLUnit membandingkan node kontrol dan pengujian pada kedalaman yang sama relatif terhadap node root .

Jadi kondisi isSimilarTo sedikit lebih menarik untuk diuji daripada kondisi isIdenticalTo . Node 3 di controlXml akan dibandingkan dengan false di testXml , secara otomatis memberikan pesan kegagalan:

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

Di sinilah DefaultNodeMatcher dan ElementSelector kelas XMLUnit berguna

The DefaultNodeMatcher kelas berkonsultasi dengan XMLUnit pada tahap perbandingan seperti loop lebih node controlXml, untuk menentukan XML simpul dari testXml untuk membandingkan dengan node XML saat itu pertemuan di controlXml .

Sebelum itu, DefaultNodeMatcher telah berkonsultasi dengan ElementSelector untuk memutuskan cara mencocokkan node.

Pengujian kami gagal karena dalam keadaan default, XMLUnit akan menggunakan pendekatan kedalaman-pertama untuk melintasi XML dan berdasarkan urutan dokumen untuk mencocokkan node, karenanya dicocokkan dengan .

Mari kita atur pengujian kita agar lulus:

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

Dalam hal ini, kami memberi tahu DefaultNodeMatcher bahwa ketika XMLUnit meminta sebuah node untuk dibandingkan, Anda seharusnya sudah mengurutkan dan mencocokkan node dengan nama elemennya.

Contoh awal yang gagal mirip dengan meneruskan ElementSelectors.Default ke DefaultNodeMatcher .

Alternatifnya, kita bisa menggunakan Diff dari xmlunit-core daripada menggunakan 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. Penilai Perbedaan Kustom

A DifferenceEvaluator membuat penentuan hasil perbandingan. Perannya dibatasi untuk menentukan tingkat keparahan hasil perbandingan.

Kelaslah yang memutuskan apakah dua buah XML itu identik , mirip atau berbeda .

Pertimbangkan potongan XML berikut:

dan:

Dalam keadaan default, mereka secara teknis dievaluasi sebagai berbeda karena atribut attr mereka memiliki nilai yang berbeda. Mari kita lihat tesnya:

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

Pesan kegagalan:

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.