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].