«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-документами, указанными выше, обнаруживаются механизмом различий.

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

Чтобы получить все различия между двумя частями 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 требует небольшой настройки с использованием функций из библиотеки xmlunit-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. Узел \u003cint\u003e3\u003c/int\u003e в controlXml будет сравниваться с \u003cboolean\u003efalse\u003c/boolean\u003e в 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>

Здесь на помощь приходят классы DefaultNodeMatcher и ElementSelector XMLUnit. удобно

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

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

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

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

@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

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, мы просто указываем его источник, а затем вызываем 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 с ValidationProblems на случай сбоя. Давайте создадим новый XML-файл с ошибками и назовем его student_with_error.xml. Вместо \u003cstudent\u003e все наши начальные теги будут \u003c/studet\u003e:

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

XPaths особенно полезны там, где Документ состоит в основном из известного неизменного содержимого и лишь небольшого количества изменяющегося содержимого, созданного системой.

9. Заключение

В этом руководстве мы представили большинство основных функций XMLUnit 2.x и то, как их использовать для проверки XML-документов в наших приложениях.

Полную реализацию всех этих примеров и фрагментов кода можно найти в проекте XMLUnit GitHub.