Introdução ao XMLUnit 2.x

Introdução ao XMLUnit 2.x

1. Visão geral

XMLUnit 2.x é uma biblioteca poderosa que nos ajuda a testar e verificar o conteúdo XML e é particularmente útil quando sabemos exatamente o que esse XML deve conter.

E então usaremos principalmente XMLUnit dentro de testes de unidadeto verify that what we have is valid XML, que contém certas informações ou está em conformidade com um determinado estilo de documento.

Além disso, com XMLUnit,we have control over what kind of difference is important to us e qual parte da referência de estilo comparar com qual parte de seu XML de comparação.

Como estamos focando no XMLUnit 2.xe não no XMLUnit 1.x, sempre que usamos a palavra XMLUnit, estamos nos referindo estritamente ao 2.x.

Por fim, também usaremos os matchers Hamcrest para asserções, então é uma boa ideia revisarHamcrest caso você não esteja familiarizado com ele.

2. Configuração do XMLUnit Maven

Para usar a biblioteca em nossos projetos maven, precisamos ter as seguintes dependências empom.xml:


    org.xmlunit
    xmlunit-core
    2.2.1

A versão mais recente dexmlunit-core pode ser encontrada seguindothis link. And:


    org.xmlunit
    xmlunit-matchers
    2.2.1

A versão mais recente dexmlunit-matchers está disponível emthis link.

3. Comparando XML

3.1. Exemplos de diferenças simples

Vamos supor que temos duas peças de XML. Eles são considerados idênticos quando o conteúdo e a sequência dos nós nos documentos são exatamente os mesmos, portanto o teste a seguir será aprovado:

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

Este próximo teste falha porque as duas partes do XML são semelhantes, mas não idênticas como seusnodes 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. Exemplo de diferença detalhada

As diferenças entre os dois documentos XML acima são detectadas peloDifference Engine.

Por padrão e por razões de eficiência, ele interrompe o processo de comparação assim que a primeira diferença é encontrada.

Para obter todas as diferenças entre duas partes do XML, usamos uma instância da classeDiff assim:

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

Se imprimirmos os valores retornados no loopwhile, o resultado é o seguinte:

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)

Cada instância descreve o tipo de diferença encontrada entre um nó de controle e um nó de teste e os detalhes desses nós (incluindo o local XPath de cada nó).

Se quisermos forçar o mecanismo de diferença parastop after the first difference is found e não continuar a enumerar outras diferenças - precisamos fornecer umComparisonController:

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

A mensagem de diferença é mais simples:

Expected element tag name 'int' but was 'boolean' -
  comparing  at /struct[1]/int[1]
    to  at /struct[1]/boolean[1] (DIFFERENT)

4. Fontes de entrada

Com XMLUnit,, podemos escolher dados XML de uma variedade de fontes que podem ser convenientes para as necessidades de nosso aplicativo. Nesse caso, usamos a classeInput com sua matriz de métodos estáticos.

Para escolher a entrada de um arquivo XML localizado na raiz do projeto, fazemos o seguinte:

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

Para escolher uma fonte de entrada de uma string XML, assim:

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

    assertThat(
      Input.fromString(testXml),isSimilarTo(Input.fromString(controlXml)));
}

Agora vamos usar um stream como entrada:

@Test
public void givenStreamAsSource_whenAbleToInput_thenCorrect() {
    assertThat(Input.fromStream(XMLUnitTests.class
      .getResourceAsStream("/test.xml")),
        isSimilarTo(
          Input.fromStream(XMLUnitTests.class
            .getResourceAsStream("/control.xml"))));
}

Também podemos usarInput.from(Object) onde passamos qualquer fonte válida para ser resolvida pelo XMLUnit.

Por exemplo, podemos passar um arquivo em:

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

Ou umString:

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

Ou umStream:

@Test
public void givenStreamAsObject_whenAbleToInput_thenCorrect() {
    assertThat(
      Input.from(XMLUnitTest.class.getResourceAsStream("/test.xml")),
      isSimilarTo(Input.from(XMLUnitTest.class.getResourceAsStream("/control.xml"))));
}

e todos eles serão resolvidos.

5. Comparando Nós Específicos

Na seção 2 acima, vimos apenas XML idêntico porque XML semelhante precisa de um pouco de personalização usando recursos da bibliotecaxmlunit-core:

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

    assertThat(testXml, isSimilarTo(controlXml));
}

O teste acima deve passar, já que os XMLs têm nós semelhantes; no entanto, ele falha. Isso ocorre porqueXMLUnit compares control and test nodes at the same depth relative to the root node.

Portanto, uma condiçãoisSimilarTo é um pouco mais interessante de testar do que uma condiçãoisIdenticalTo. O nó<int>3</int> emcontrolXml será comparado com<boolean>false</boolean> emtestXml, dando automaticamente a mensagem de falha:

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

É aqui que as classesDefaultNodeMatcher eElementSelector do XMLUnit são úteis

A classeDefaultNodeMatcher é consultada por XMLUnit no estágio de comparação enquanto faz um loop nos nós decontrolXml, para determinar qual nó XML detestXml comparar com o nó XML atual que encontra emcontrolXml.

Antes disso,DefaultNodeMatcher já terá consultadoElementSelector para decidir como combinar os nós.

Nosso teste falhou porque no estado padrão, XMLUnit usará uma abordagem em profundidade para percorrer os XMLs e com base na ordem do documento para combinar os nós, portanto,<int> é correspondido com<boolean>.

Vamos ajustar nosso teste para que ele passe:

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

    assertThat(testXml,
      isSimilarTo(controlXml).withNodeMatcher(
      new DefaultNodeMatcher(ElementSelectors.byName)));
}

Nesse caso, estamos dizendo aDefaultNodeMatcher que quando o XMLUnit pede um nó para comparar, você já deve ter classificado e correspondido os nós por seus nomes de elemento.

O exemplo inicial com falha foi semelhante a passarElementSelectors.Default paraDefaultNodeMatcher.

Alternativamente, poderíamos ter usado umDiff dexmlunit-core em vez de usarxmlunit-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 personalizado

ADifferenceEvaluator determina o resultado de uma comparação. Seu papel é restrito a determinar a gravidade do resultado de uma comparação.

É a classe que decide se duas partes XML sãoidentical,similar oudifferent.

Considere as seguintes partes XML:

and:

No estado padrão, eles são avaliados tecnicamente como diferentes porque seus atributosattr têm valores diferentes. Vamos dar uma olhada em um teste:

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

Mensagem de falha:

java.lang.AssertionError: Expected attribute value 'abc' but was 'xyz' -
  comparing  at /a[1]/b[1]/@attr
  to  at /a[1]/b[1]/@attr

Se realmente não nos importamos com o atributo, podemos mudar o comportamento deDifferenceEvaluator para ignorá-lo. Fazemos isso criando nosso próprio:

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

Em seguida, reescrevemos nosso teste inicial com falha e fornecemos nossa própria instânciaDifferenceEvaluator, assim:

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

Desta vez passa.

7. Validação

XMLUnit executa validação XML usando a classeValidator. Você cria uma instância dele usando o método de fábricaforLanguage enquanto passa o esquema para usar na validação.

O esquema é passado como um URI que leva à sua localização, XMLUnit abstrai as localizações do esquema que ele suporta na classeLanguages como constantes.

Normalmente criamos uma instância da classeValidator assim:

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

Após esta etapa, se tivermos nosso próprio arquivo XSD para validar em relação ao nosso XML, simplesmente especificamos sua fonte e chamamos o métodoValidatorvalidateInstance com nossa fonte de arquivo XML.

Veja, por exemplo, nossostudents.xsd:



    
        
            
                
            
        
    
    
        
            
            
        
        
    

Estudents.xml:



    
        Rajiv
        18
    
    
        Candie
        19
    

Vamos então fazer um teste:

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

O resultado da validação é uma instância deValidationResult que contém um sinalizador booleano indicando se o documento foi validado com sucesso.

OValidationResult também contém umIterable comValidationProblems no caso de haver uma falha. Vamos criar um novo XML com erros chamadosstudents_with_error.xml. Em vez de<student>, nossas tags iniciais são todas</studet>:



    
        Rajiv
        18
    
    
        Candie
        19
    

Em seguida, execute este teste:

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

Se imprimíssemos os erros no loopwhile, eles se pareceriam com:

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

Quando uma expressão XPath é avaliada em relação a um pedaço de XML, umNodeList é criado contendo osNodes. correspondentes

Considere este pedaço de XML salvo em um arquivo chamadoteachers.xml:


    
        math
        physics
    
    
        political education
        english
    

O XMLUnit oferece vários métodos de asserção relacionados ao XPath, conforme demonstrado abaixo.

Podemos recuperar todos os nós chamadosteachere realizar asserções neles individualmente:

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

Observe como validamos o número de nós filhos, o nome de cada nó e os atributos em cada nó. Muitas outras opções estão disponíveis após recuperar oNode.

Para verificar se existe um caminho, podemos fazer o seguinte:

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

Para verificar se um caminho não existe, é isso que podemos fazer:

@Test
public void givenXmlSource_whenFailsToValidateInExistentXPath_thenCorrect() {
    assertThat(Input.fromFile(new File("teachers.xml")), not(hasXPath("//sujet")));
}

Os XPaths são especialmente úteis nos casos em que um documento é composto basicamente por conteúdo conhecido e imutável, com apenas uma pequena quantidade de conteúdo alterado criada pelo sistema.

9. Conclusão

Neste tutorial, apresentamos a maioria dos recursos básicos deXMLUnit 2.x e como usá-los para validar documentos XML em nossos aplicativos.

A implementação completa de todos esses exemplos e snippets de código pode ser encontrada emXMLUnitGitHub project.