Guia de testes dinâmicos em junho 5

Guia de testes dinâmicos em junho 5

1. Visão geral

O teste dinâmico é um novo modelo de programação introduzido no JUnit 5. Neste artigo, veremos o que são exatamente os testes dinâmicos e como criá-los.

Se você for completamente novo no JUnit 5, pode verificar ospreview of JUnit 5 eour primary guide.

2. O que é umDynamicTest?

Os testes padrão anotados com a anotação@Test são testes estáticos que são totalmente especificados no momento da compilação. A DynamicTest is a test generated during runtime. Esses testes são gerados por um método de fábrica anotado com a anotação@TestFactory.

Um método@TestFactory deve retornar aStream,Collection,Iterable ouIterator deDynamicTest instâncias. Retornar qualquer outra coisa resultará emJUnitException, pois os tipos de retorno inválidos não podem ser detectados em tempo de compilação. Além disso, um método@TestFactory não pode ser static ouprivate.

OsDynamicTests são executados de forma diferente do@Tests padrão e não oferecem suporte a retornos de chamada do ciclo de vida. Ou seja, o@BeforeEach and the @AfterEach methods will not be called for the DynamicTests.

3. CriandoDynamicTests

Primeiro, vamos dar uma olhada nas diferentes maneiras de criarDynamicTests.

Os exemplos aqui não são dinâmicos por natureza, mas eles fornecerão um bom ponto de partida para criar exemplos verdadeiramente dinâmicos.

Vamos criar umCollection deDynamicTest:

@TestFactory
Collection dynamicTestsWithCollection() {
    return Arrays.asList(
      DynamicTest.dynamicTest("Add test",
        () -> assertEquals(2, Math.addExact(1, 1))),
      DynamicTest.dynamicTest("Multiply Test",
        () -> assertEquals(4, Math.multiplyExact(2, 2))));
}

O método@TestFactory informa ao JUnit que esta é uma fábrica para a criação de testes dinâmicos. Como podemos ver, estamos retornando apenas umCollection deDynamicTest. Each of the DynamicTest consists of two parts, the name of the test or the display name, and an Executable.

A saída conterá o nome de exibição que passamos para os testes dinâmicos:

Add test(dynamicTestsWithCollection())
Multiply Test(dynamicTestsWithCollection())

O mesmo teste pode ser modificado para retornar umIterable,Iterator ou umStream:

@TestFactory
Iterable dynamicTestsWithIterable() {
    return Arrays.asList(
      DynamicTest.dynamicTest("Add test",
        () -> assertEquals(2, Math.addExact(1, 1))),
      DynamicTest.dynamicTest("Multiply Test",
        () -> assertEquals(4, Math.multiplyExact(2, 2))));
}

@TestFactory
Iterator dynamicTestsWithIterator() {
    return Arrays.asList(
      DynamicTest.dynamicTest("Add test",
        () -> assertEquals(2, Math.addExact(1, 1))),
      DynamicTest.dynamicTest("Multiply Test",
        () -> assertEquals(4, Math.multiplyExact(2, 2))))
        .iterator();
}

@TestFactory
Stream dynamicTestsFromIntStream() {
    return IntStream.iterate(0, n -> n + 2).limit(10)
      .mapToObj(n -> DynamicTest.dynamicTest("test" + n,
        () -> assertTrue(n % 2 == 0)));
}

Observe que se@TestFactory retornar umStream, ele será fechado automaticamente quando todos os testes forem executados.

A saída será praticamente a mesma do primeiro exemplo. Ele conterá o nome de exibição que passamos para o teste dinâmico.

4. Criando umStream deDynamicTests

Para fins de demonstração, considere umDomainNameResolver que retorna um endereço IP quando passamos o nome de domínio como entrada.

Para simplificar, vamos dar uma olhada no esqueleto de alto nível de nosso método de fábrica:

@TestFactory
Stream dynamicTestsFromStream() {

    // sample input and output
    List inputList = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List outputList = Arrays.asList(
      "154.174.10.56", "211.152.104.132", "178.144.120.156");

    // input generator that generates inputs using inputList
    /*...code here...*/

    // a display name generator that creates a
    // different name based on the input
    /*...code here...*/

    // the test executor, which actually has the
    // logic to execute the test case
    /*...code here...*/

    // combine everything and return a Stream of DynamicTest
    /*...code here...*/
}

Não há muito código relacionado aDynamicTest aqui, além da anotação@TestFactory, com a qual já estamos familiarizados.

Os doisArrayLists serão usados ​​como entrada paraDomainNameResolvere saída esperada, respectivamente.

Vamos agora dar uma olhada no gerador de entrada:

Iterator inputGenerator = inputList.iterator();

O gerador de entrada nada mais é do que umIterator deString. Ele usa nossoinputListe retorna o nome de domínio um por um.

O gerador de nome para exibição é bastante simples:

Function displayNameGenerator
  = (input) -> "Resolving: " + input;

A tarefa de um gerador de nome para exibição é apenas fornecer um nome para exibição do caso de teste que será usado nos relatórios JUnit ou na guia JUnit do nosso IDE.

Aqui estamos apenas utilizando o nome de domínio para gerar nomes exclusivos para cada teste. Não é necessário criar nomes exclusivos, mas ajudará em caso de falha. Tendo isso, seremos capazes de dizer o nome de domínio para o qual o caso de teste falhou.

Agora vamos dar uma olhada na parte central do nosso teste - o código de execução do teste:

DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer testExecutor = (input) -> {
    int id = inputList.indexOf(input);

    assertEquals(outputList.get(id), resolver.resolveDomain(input));
};

UsamosThrowingConsumer, que é um@FunctionalInterface para escrever o caso de teste. Para cada entrada gerada pelo gerador de dados, buscamos a saída esperada deoutputListe a saída real de uma instância deDomainNameResolver.

Agora a última parte é simplesmente montar todas as peças e retornar como umStream deDynamicTest:

return DynamicTest.stream(
  inputGenerator, displayNameGenerator, testExecutor);

É isso aí. A execução do teste exibirá o relatório contendo os nomes definidos pelo nosso gerador de nomes para exibição:

Resolving: www.somedomain.com(dynamicTestsFromStream())
Resolving: www.anotherdomain.com(dynamicTestsFromStream())
Resolving: www.yetanotherdomain.com(dynamicTestsFromStream())

5. Melhorando osDynamicTest usando recursos do Java 8

A fábrica de teste escrita na seção anterior pode ser drasticamente aprimorada usando os recursos do Java 8. O código resultante será muito mais limpo e pode ser escrito em um número menor de linhas:

@TestFactory
Stream dynamicTestsFromStreamInJava8() {

    DomainNameResolver resolver = new DomainNameResolver();

    List domainNames = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List outputList = Arrays.asList(
      "154.174.10.56", "211.152.104.132", "178.144.120.156");

    return inputList.stream()
      .map(dom -> DynamicTest.dynamicTest("Resolving: " + dom,
        () -> {int id = inputList.indexOf(dom);

      assertEquals(outputList.get(id), resolver.resolveDomain(dom));
    }));
}

O código acima tem o mesmo efeito que vimos na seção anterior. OinputList.stream().map() fornece o fluxo de entradas (gerador de entrada). O primeiro argumento paradynamicTest() é nosso gerador de nome de exibição (“Resolvendo:” +dom) enquanto o segundo argumento, alambda, é nosso executor de teste.

A saída será a mesma da seção anterior.

6. Exemplo Adicional

Neste exemplo, estamos explorando ainda mais o poder dos testes dinâmicos para filtrar as entradas com base nos casos de teste:

@TestFactory
Stream dynamicTestsForEmployeeWorkflows() {
    List inputList = Arrays.asList(
      new Employee(1, "Fred"), new Employee(2), new Employee(3, "John"));

    EmployeeDao dao = new EmployeeDao();
    Stream saveEmployeeStream = inputList.stream()
      .map(emp -> DynamicTest.dynamicTest(
        "saveEmployee: " + emp.toString(),
          () -> {
              Employee returned = dao.save(emp.getId());
              assertEquals(returned.getId(), emp.getId());
          }
    ));

    Stream saveEmployeeWithFirstNameStream
      = inputList.stream()
      .filter(emp -> !emp.getFirstName().isEmpty())
      .map(emp -> DynamicTest.dynamicTest(
        "saveEmployeeWithName" + emp.toString(),
        () -> {
            Employee returned = dao.save(emp.getId(), emp.getFirstName());
            assertEquals(returned.getId(), emp.getId());
            assertEquals(returned.getFirstName(), emp.getFirstName());
        }));

    return Stream.concat(saveEmployeeStream,
      saveEmployeeWithFirstNameStream);
}

O métodosave(Long) precisa apenas doemployeeId. Portanto, ele utiliza todas as instânciasEmployee. O métodosave(Long, String) precisa defirstName além deemployeeId. Portanto, ele filtra as instâncias deEmployee semfirstName.

Finalmente, combinamos os dois fluxos e retornamos todos os testes como um únicoStream.

Agora, vamos dar uma olhada na saída:

saveEmployee: Employee
  [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows())
saveEmployee: Employee
  [id=2, firstName=](dynamicTestsForEmployeeWorkflows())
saveEmployee: Employee
  [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())
saveEmployeeWithNameEmployee
  [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows())
saveEmployeeWithNameEmployee
  [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())

7. Conclusão

Os testes parametrizados podem substituir muitos dos exemplos neste artigo. However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don’t.

Além disso, testes dinâmicos fornecem mais flexibilidade em relação à maneira como a entrada é gerada e como os testes são executados.

JUnit 5 prefers extensions over features principle. Como resultado, o objetivo principal dos testes dinâmicos é fornecer um ponto de extensão para estruturas ou extensões de terceiros.

Você pode ler mais sobre outros recursos do JUnit 5 em nossoarticle on repeated tests in JUnit 5.

Não se esqueça de verificar o código-fonte completo destearticle on GitHub.