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 .