Перейти к основному содержимому

Введение в XMLUnit 2.x

· 10 мин. чтения

1. Обзор

XMLUnit 2.x — это мощная библиотека, которая помогает нам тестировать и проверять XML-содержимое и особенно удобна, когда мы точно знаем, что должен содержать этот XML.

И поэтому мы будем в основном использовать XMLUnit внутри модульных тестов , чтобы убедиться, что то, что у нас есть, является допустимым XML , что он содержит определенную информацию или соответствует определенному стилю документа.

Кроме того, с помощью XMLUnit мы можем контролировать, какие различия важны для нас и какую часть ссылки на стиль сравнивать с какой частью сравниваемого XML.

Поскольку мы фокусируемся на XMLUnit 2.x, а не на XMLUnit 1.x, всякий раз, когда мы используем слово XMLUnit, мы строго ссылаемся на 2.x.

Наконец, мы также будем использовать сопоставители Hamcrest для утверждений, поэтому рекомендуется освежить в памяти Hamcrest , если вы с ним не знакомы.

2. Настройка XMLUnit Maven

Чтобы использовать библиотеку в наших проектах maven, нам нужно иметь следующие зависимости в pom.xml :

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

Последнюю версию xmunit-core можно найти по этой ссылке . А также:

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

Последняя версия xmlunit-matchers доступна по этой ссылке .

3. Сравнение XML

3.1. Примеры простых различий

Предположим, у нас есть два фрагмента 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));
}

Следующий тест не пройден, так как две части 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. Подробный пример отличия

Различия между двумя XML-документами, указанными выше, обнаруживаются с помощью Difference Engine .

По умолчанию и из соображений эффективности он останавливает процесс сравнения, как только обнаруживается первое отличие.

Чтобы получить все различия между двумя частями 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 каждого узла).

Если мы хотим заставить Difference Engine остановиться после того, как будет найдено первое отличие, и не переходить к перечислению дальнейших различий, нам нужно предоставить 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 выше мы рассмотрели только идентичный XML, потому что аналогичный XML требует небольшой настройки с использованием функций из библиотеки xmunit-core :

@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 . Узел <int>3</int> в controlXml будет сравниваться с <boolean>false</boolean> в testXml , автоматически выдавая сообщение об ошибке:

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 .

XMLUnit консультируется с классом DefaultNodeMatcher на этапе сравнения, поскольку он перебирает узлы controlXml, чтобы определить, какой узел XML из testXml сравнивать с текущим узлом XML, с которым он сталкивается в controlXml .

Перед этим DefaultNodeMatcher уже проконсультировался с ElementSelector , чтобы решить, как сопоставлять узлы.

Наш тест провалился, потому что в состоянии по умолчанию XMLUnit будет использовать подход в глубину для обхода XML-файлов и на основе порядка документов для сопоставления узлов, поэтому <int> сопоставляется с <boolean> .

Давайте настроим наш тест так, чтобы он прошел:

@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.Default в DefaultNodeMatcher .

В качестве альтернативы мы могли бы использовать Diff из xmlunit-core , а не использовать xmlunit-matchers :

@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 определяет результат сравнения. Его роль ограничивается определением серьезности результата сравнения.

Именно класс решает, являются ли две части 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 выполняет проверку XML с помощью класса Validator . Вы создаете его экземпляр с помощью фабричного метода forLanguage , передавая схему для использования при проверке.

Схема передается как URI, ведущий к ее местоположению, XMLUnit абстрагирует местоположения схемы, которые он поддерживает в классе Languages , как константы.

Обычно мы создаем экземпляр класса Validator следующим образом:

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

После этого шага, если у нас есть собственный XSD-файл для проверки на соответствие нашему XML, мы просто указываем его источник, а затем вызываем метод Validator validateInstance с нашим источником XML-файла.

Возьмем, к примеру, наш student.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>

И student.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 также содержит Iterable с ValidationProblem на случай сбоя. Давайте создадим новый XML-файл с ошибками и назовем его student_with_error.xml. Вместо <student> все наши начальные теги </studet> : ``

<?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 , содержащий соответствующие узлы.

Рассмотрим этот фрагмент XML, сохраненный в файле с именем Teachers.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, как показано ниже.

Мы можем получить все узлы, называемые учителем , и выполнить утверждения для них по отдельности:

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

Обратите внимание, как мы проверяем количество дочерних узлов, имя каждого узла и атрибуты в каждом узле. После получения файла Node .

Чтобы убедиться, что путь существует, мы можем сделать следующее:

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