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

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

1. обзор

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

И поэтому мы в основном будем использовать XMLUnit внутри модульных тестовto verify that what we have is valid XML, которые содержат определенную информацию или соответствуют определенному стилевому документу.

Кроме того, с XMLUnitwe have control over what kind of difference is important to us и какая часть ссылки на стиль сравнивать с какой частью вашего сравнительного XML.

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

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

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

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


    org.xmlunit
    xmlunit-core
    2.2.1

Последнюю версиюxmlunit-core можно найти, выполнивthis link. And:


    org.xmlunit
    xmlunit-matchers
    2.2.1

Последняя версияxmlunit-matchers доступна по адресуthis link.

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

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

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

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

Следующий тест не проходит, поскольку две части XML похожи, но не идентичны, как ихnodes occur in a different sequence:

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

3.2. Подробный пример разницы

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

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

Чтобы увидеть все различия между двумя частями XML, мы используем экземпляр классаDiff следующим образом:

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

Если мы распечатаем значения, возвращенные в циклеwhile, результат будет следующим:

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)

Каждый экземпляр описывает как тип различий, обнаруженных между управляющим узлом и тестовым узлом, так и подробности этих узлов (включая местоположение XPath каждого узла).

Если мы хотим принудительно установить разностную машину наstop after the first difference is found и не переходить к перечислению дальнейших различий - нам нужно предоставить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));
}

Сообщение о разнице проще:

Expected element tag name 'int' but was 'boolean' -
  comparing  at /struct[1]/int[1]
    to  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 = "3false";
    String testXml = "3false";

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

Или aString:

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

Или aStream:

@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 = "3false";
    String testXml = "false3";

    assertThat(testXml, isSimilarTo(controlXml));
}

Вышеупомянутый тест должен пройти, так как у XML есть подобные узлы, однако, это терпит неудачу. Это потому, чтоXMLUnit compares control and test nodes at the same depth relative to the root node.

Так что условиеisSimilarTo немного интереснее проверить, чем условиеisIdenticalTo. Узел<int>3</int> вcontrolXml будет сравниваться с<boolean>false</boolean> вtestXml, автоматически выдав сообщение об ошибке:

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

Здесь пригодятся классыDefaultNodeMatcher иElementSelector XMLUnit.

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

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

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

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

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

    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 = "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. ПользовательскийDifferenceEvaluator

ADifferenceEvaluator определяет результат сравнения. Его роль ограничивается определением серьезности результата сравнения.

Класс, который решает, будут ли две части XML:identical,similar илиdifferent.

Рассмотрим следующие фрагменты XML:

and:

В состоянии по умолчанию они технически оцениваются как разные, потому что их атрибутыattr имеют разные значения. Давайте посмотрим на тест:

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

Сообщение об ошибке:

java.lang.AssertionError: Expected attribute value 'abc' but was 'xyz' -
  comparing  at /a[1]/b[1]/@attr
  to  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 = "";
    final String test = "";
    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‘svalidateInstance с нашим источником XML-файла.

Возьмем, к примеру, нашstudents.xsd:



    
        
            
                
            
        
    
    
        
            
            
        
        
    

Иstudents.xml:



    
        Rajiv
        18
    
    
        Candie
        19
    

А затем проведем тест:

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

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

ValidationResult также содержитIterable сValidationProblems на случай отказа. Давайте создадим новый XML с ошибками под названиемstudents_with_error.xml. Вместо<student> все наши начальные теги будут</studet>:



    
        Rajiv
        18
    
    
        Candie
        19
    

Затем запустите этот тест против него:

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

Если бы мы распечатали ошибки в цикле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 "".' }
ValidationProblem { line=6, column=4, type=ERROR, message='The element type "studet"
  must be terminated by the matching end-tag "".' }

8. XPath

Когда выражение XPath сравнивается с фрагментом XML, создаетсяNodeList, содержащий совпадающийNodes.

Рассмотрим этот фрагмент XML, сохраненный в файле с именемteachers.xml:


    
        math
        physics
    
    
        political education
        english
    

XMLUnit предлагает несколько методов подтверждения, связанных с XPath, как показано ниже.

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

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

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

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