Uma comparação rápida JUnit vs TestNG

Uma comparação rápida JUnit vs TestNG

*1. Visão geral *

JUnit e TestNG são, sem dúvida, as duas estruturas de teste de unidade mais populares no ecossistema Java. Embora o JUnit inspire o próprio TestNG, ele fornece seus recursos distintos e, ao contrário do JUnit, funciona para níveis funcionais e mais altos de teste.

Neste post,* discutiremos e comparamos essas estruturas, abordando seus recursos e casos de uso comuns *.

2. Configuração de teste

Ao escrever casos de teste, geralmente precisamos executar algumas instruções de configuração ou inicialização antes das execuções de teste e também alguma limpeza após a conclusão dos testes. Vamos avaliar isso nas duas estruturas.

*O JUnit oferece inicialização e limpeza em dois níveis, antes e depois de cada método e classe.* Temos anotações _ @ BeforeEach_, _ @ AfterEach_ no nível do método e _ @ BeforeAll_ e _ @ AfterAll_ no nível da classe:
public class SummationServiceTest {

    private static List<Integer> numbers;

    @BeforeAll
    public static void initialize() {
        numbers = new ArrayList<>();
    }

    @AfterAll
    public static void tearDown() {
        numbers = null;
    }

    @BeforeEach
    public void runBeforeEachTest() {
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
    }

    @AfterEach
    public void runAfterEachTest() {
        numbers.clear();
    }

    @Test
    public void givenNumbers_sumEquals_thenCorrect() {
        int sum = numbers.stream().reduce(0, Integer::sum);
        assertEquals(6, sum);
    }
}

Observe que este exemplo usa o JUnit 5. Na versão anterior do JUnit 4, precisaríamos usar as anotações _ @ Before_ e _ @ After_, que são equivalentes a _ @ BeforeEach_ e @AfterEach. Da mesma forma, _ @ BeforeAll_ e _ @ AfterAll_ são substitutos para os _ @ BeforeClass_ e J@UnifiedClass da JUnit 4.

Semelhante ao JUnit, o TestNG também fornece inicialização e limpeza no nível do método e da classe . Enquanto _ @ BeforeClass_ e _ @ AfterClass_ permanecem os mesmos no nível da classe, as anotações no nível do método são @BeforeMethod e _ @ AfterMethod: _

@BeforeClass
public void initialize() {
    numbers = new ArrayList<>();
}

@AfterClass
public void tearDown() {
    numbers = null;
}

@BeforeMethod
public void runBeforeEachTest() {
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
}

@AfterMethod
public void runAfterEachTest() {
    numbers.clear();
}
*O TestNG também oferece anotações _ @ BeforeSuite, @AfterSuite, @BeforeGroup e @AfterGroup_, para configurações nos níveis de suíte e grupo:
@BeforeGroups("positive_tests")
public void runBeforeEachGroup() {
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
}

@AfterGroups("negative_tests")
public void runAfterEachGroup() {
    numbers.clear();
}

Além disso, podemos usar o _ @ BeforeTest_ e o @AfterTest se precisarmos de qualquer configuração antes ou depois dos casos de teste incluídos na tag _ <test> _ no arquivo de configuração XML do TestNG:

<test name="test setup">
    <classes>
        <class name="SummationServiceTest">
            <methods>
                <include name="givenNumbers_sumEquals_thenCorrect"/>
            </methods>
        </class>
    </classes>
</test>

Observe que a declaração dos métodos _ @ BeforeClass_ e _ @ AfterClass_ deve ser estática no JUnit. Por comparação, a declaração do método TestNG não possui essas restrições.

===* 3. Ignorando testes *

Ambas as estruturas suportam ignorar os casos de teste, embora o façam de maneira bem diferente. O JUnit oferece a anotação _ @ Ignore_:

@Ignore
@Test
public void givenNumbers_sumEquals_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

enquanto TestNG usa _ @ Test_ com um parâmetro "ativado" com um valor booleano true ou false:

@Test(enabled=false)
public void givenNumbers_sumEquals_thenCorrect() {
    int sum = numbers.stream.reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

===* 4. Executando testes juntos *

A execução de testes juntos como uma coleção é possível no JUnit e no TestNG, mas eles fazem isso de maneiras diferentes.

*Podemos usar as anotações _ @ RunWith, _ _ @ SelectPackages_ e _ @ SelectClasses_ para agrupar casos de teste e executá-los como um conjunto na _JUnit 5 _.* Um conjunto é uma coleção de casos de teste que podemos agrupar e executar como um único teste.

Se queremos agrupar casos de teste de pacotes diferentes para executar juntos em um Suite , precisamos da anotação _ @ SelectPackages_:

@RunWith(JUnitPlatform.class)
@SelectPackages({ "org..java.suite.childpackage1", "org..java.suite.childpackage2" })
public class SelectPackagesSuiteUnitTest {

}

Se queremos que classes de teste específicas sejam executadas juntas, a JUnit 5 fornece flexibilidade através de _ @ SelectClasses_:

@RunWith(JUnitPlatform.class)
@SelectClasses({Class1UnitTest.class, Class2UnitTest.class})
public class SelectClassesSuiteUnitTest {

}

Anteriormente, usando o JUnit 4, conseguimos agrupar e executar vários testes juntos usando a anotação @ Suite :

@RunWith(Suite.class)
@Suite.SuiteClasses({ RegistrationTest.class, SignInTest.class })
public class SuiteTest {

}
*No TestNG, podemos agrupar testes usando um arquivo XML:*
<suite name="suite">
    <test name="test suite">
        <classes>
            <class name="com..RegistrationTest"/>
            <class name="com..SignInTest"/>
        </classes>
    </test>
</suite>

Isso indica que RegistrationTest e SignInTest serão executados juntos.

Além do agrupamento de classes, o TestNG também pode agrupar métodos usando a anotação @_Test (groups = ”groupName”) _:

@Test(groups = "regression")
public void givenNegativeNumber_sumLessthanZero_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertTrue(sum < 0);
}

Vamos usar um XML para executar os grupos:

<test name="test groups">
    <groups>
        <run>
            <include name="regression"/>
        </run>
    </groups>
    <classes>
        <class
          name="com..SummationServiceTest"/>
    </classes>
</test>

Isso executará o método de teste marcado com o grupo regression.

*5. Testando exceções *

*O recurso para testar exceções usando anotações está disponível no JUnit e no TestNG.*

Vamos primeiro criar uma classe com um método que lança uma exceção:

public class Calculator {
    public double divide(double a, double b) {
        if (b == 0) {
            throw new DivideByZeroException("Divider cannot be equal to zero!");
        }
        return a/b;
    }
}

No JUnit 5, podemos usar o assertThrows API para testar exceções:

@Test
public void whenDividerIsZero_thenDivideByZeroExceptionIsThrown() {
    Calculator calculator = new Calculator();
    assertThrows(DivideByZeroException.class, () -> calculator.divide(10, 0));
}

Na JUnit 4, podemos conseguir isso usando @ Test (esperado = DivideByZeroException.class) sobre a API de teste.

E com o TestNG também podemos implementar o mesmo:

@Test(expectedExceptions = ArithmeticException.class)
public void givenNumber_whenThrowsException_thenCorrect() {
    int i = 1/0;
}

Esse recurso implica que exceção é lançada em um trecho de código, que faz parte de um teste.

*6. Testes parametrizados *

Testes de unidade parametrizados são úteis para testar o mesmo código sob várias condições. Com a ajuda de testes de unidade parametrizados, podemos configurar um método de teste que obtém dados de alguma fonte de dados. A idéia principal é tornar o método de teste de unidade reutilizável e testar com um conjunto diferente de entradas.

*No _JUnit 5_, temos a vantagem de os métodos de teste consumirem argumentos de dados diretamente da fonte configurada.* Por padrão, o JUnit 5 fornece algumas anotações de _source_ como:
  • _ @ ValueSource: _ podemos usar isso com uma matriz de valores do tipo _Short, Byte, Int, Long, Float, Double, Char, _ e _String: _

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void givenString_TestNullOrNot(String word) {
    assertNotNull(word);
}
  • @ EnumSource - passa Enum constantes como parâmetros para o método de teste:

@ParameterizedTest
@EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"})
void givenEnum_TestContainsOrNot(PizzaDeliveryStrategy timeUnit) {
    assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit));
}
  • @ MethodSource - passes métodos externos que geram fluxos:

static Stream<String> wordDataProvider() {
    return Stream.of("foo", "bar");
}

@ParameterizedTest
@MethodSource("wordDataProvider")
void givenMethodSource_TestInputStream(String argument) {
    assertNotNull(argument);
}
  • _ @ CsvSource –_ usa valores CSV como fonte para os parâmetros:

@ParameterizedTest
@CsvSource({ "1, Car", "2, House", "3, Train" })
void givenCSVSource_TestContent(int id, String word) {
    assertNotNull(id);
    assertNotNull(word);
}

Da mesma forma, temos outras fontes como @ CsvFileSource se precisarmos ler um arquivo CSV do classpath e _ @ ArgumentSource_ para especificar um ArgumentsProvider. personalizado e reutilizável.

Na JUnit 4, a classe de teste deve ser anotada com _ @ RunWith_ para torná-la uma classe parametrizada e _ @ Parameter_ para usar o valor de parâmetro para teste de unidade.

*No TestNG, podemos parametrizar testes usando as anotações @_Parameter_ ou _ @ DataProvider_.* Enquanto estiver usando o arquivo XML, anote o método de teste com @_Parameter: _
@Test
@Parameters({"value", "isEven"})
public void
  givenNumberFromXML_ifEvenCheckOK_thenCorrect(int value, boolean isEven) {
    Assert.assertEquals(isEven, value % 2 == 0);
}

e forneça os dados no arquivo XML:

<suite name="My test suite">
    <test name="numbersXML">
        <parameter name="value" value="1"/>
        <parameter name="isEven" value="false"/>
        <classes>
            <class name=".com.ParametrizedTests"/>
        </classes>
    </test>
</suite>

Embora o uso de informações no arquivo XML seja simples e útil, em alguns casos, pode ser necessário fornecer dados mais complexos.

Para isso, podemos usar a anotação _ @ DataProvider_ que nos permite mapear tipos de parâmetros complexos para métodos de teste.

Aqui está um exemplo do uso de _ @ DataProvider_ para tipos de dados primitivos:

@DataProvider(name = "numbers")
public static Object[][] evenNumbers() {
    return new Object[][]{{1, false}, {2, true}, {4, true}};
}

@Test(dataProvider = "numbers")
public void givenNumberFromDataProvider_ifEvenCheckOK_thenCorrect
  (Integer number, boolean expected) {
    Assert.assertEquals(expected, number % 2 == 0);
}

E @ DataProvider para objetos:

@Test(dataProvider = "numbersObject")
public void givenNumberObjectFromDataProvider_ifEvenCheckOK_thenCorrect
  (EvenNumber number) {
    Assert.assertEquals(number.isEven(), number.getValue() % 2 == 0);
}

@DataProvider(name = "numbersObject")
public Object[][] parameterProvider() {
    return new Object[][]{{new EvenNumber(1, false)},
      {new EvenNumber(2, true)}, {new EvenNumber(4, true)}};
}

Da mesma maneira, quaisquer objetos específicos a serem testados podem ser criados e retornados usando o provedor de dados. É útil ao integrar com estruturas como o Spring.

Observe que, no TestNG, como o método _ @ DataProvider_ não precisa ser estático, podemos usar vários métodos de provedor de dados na mesma classe de teste.

*7. Tempo limite do teste *

Testes com tempo limite esgotado significa que um caso de teste falhará se a execução não for concluída dentro de um determinado período especificado. O JUnit e o TestNG suportam testes de tempo limite excedido. Na JUnit 5, podemos escrever um teste de tempo limite como :

@Test
public void givenExecution_takeMoreTime_thenFail() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(10000));
}

Na JUnit 4 e no TestNG, podemos fazer o mesmo teste usando @_Test (timeout = 1000) _

@Test(timeOut = 1000)
public void givenExecution_takeMoreTime_thenFail() {
    while (true);
}

===* 8. Testes dependentes *

TestNG suporta testes de dependência. Isso significa que, em um conjunto de métodos de teste, se o teste inicial falhar, todos os testes dependentes subsequentes serão ignorados, não marcados como falhos, como no caso da JUnit.

Vamos dar uma olhada em um cenário em que precisamos validar o email e, se for bem-sucedido, procederemos ao login:

@Test
public void givenEmail_ifValid_thenTrue() {
    boolean valid = email.contains("@");
    Assert.assertEquals(valid, true);
}

@Test(dependsOnMethods = {"givenEmail_ifValid_thenTrue"})
public void givenValidEmail_whenLoggedIn_thenTrue() {
    LOGGER.info("Email {} valid >> logging in", email);
}

===* 9. Ordem de execução do teste *

*Não há uma ordem implícita definida na qual os métodos de teste serão executados no JUnit 4. ou TestNG.* Os métodos são chamados apenas conforme retornados pela API Java Reflection. Desde o JUnit 4, ele usa uma ordem mais determinística, mas não previsível.

Para ter mais controle, anotamos a classe de teste com a anotação _ @ FixMethodOrder_ e mencionamos um classificador de método:

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SortedTests {

    @Test
    public void a_givenString_whenChangedtoInt_thenTrue() {
        assertTrue(
          Integer.valueOf("10") instanceof Integer);
    }

    @Test
    public void b_givenInt_whenChangedtoString_thenTrue() {
        assertTrue(
          String.valueOf(10) instanceof String);
    }

}

O parâmetro MethodSorters.NAME_ASCENDING classifica os métodos pelo nome do método, em ordem lexicográfica. Além desse classificador, temos MethodSorter.DEFAULT e MethodSorter.JVM também.

Enquanto o TestNG também fornece algumas maneiras de ter controle na ordem de execução do método de teste. Fornecemos o parâmetro priority na anotação _ @ Test_:

@Test(priority = 1)
public void givenString_whenChangedToInt_thenCorrect() {
    Assert.assertTrue(
      Integer.valueOf("10") instanceof Integer);
}

@Test(priority = 2)
public void givenInt_whenChangedToString_thenCorrect() {
    Assert.assertTrue(
      String.valueOf(23) instanceof String);
}

Observe que essa prioridade chama métodos de teste com base na prioridade, mas não garante que os testes em um nível sejam concluídos antes de chamar o próximo nível de prioridade.

Às vezes, ao escrever casos de teste funcionais no TestNG, podemos ter um teste interdependente em que a ordem de execução deve ser a mesma para cada execução de teste. Para conseguir isso, devemos usar o parâmetro dependsOnMethods na anotação @Test, como vimos na seção anterior.

10. Nome do teste personalizado

Por padrão, sempre que executamos um teste, a classe de teste e o nome do método de teste são impressos no console ou no IDE. JUnit 5 fornece um recurso exclusivo, onde podemos mencionar nomes descritivos personalizados para métodos de classe e teste usando a anotação _ @ DisplayName_.

Essa anotação não oferece nenhum benefício de teste, mas também facilita a leitura e a compreensão dos resultados de testes para uma pessoa não técnica:

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
@DisplayName("Test Method to check that the inputs are not nullable")
void givenString_TestNullOrNot(String word) {
    assertNotNull(word);
}

Sempre que executamos o teste, a saída mostrará o nome de exibição em vez do nome do método.

No momento, em TestNG, não há como fornecer um nome personalizado.

11. Conclusão

O JUnit e o TestNG são ferramentas modernas para testes no ecossistema Java.

Neste artigo, vimos rapidamente várias maneiras de escrever testes com cada uma dessas duas estruturas de teste.

A implementação de todos os trechos de código pode ser encontrada em TestNG e https://github.com/eugenp/tutorials/tree/master/testing-modules/junit5-migration [projeto junit-5 Github].