Une comparaison rapide entre JUnit et TestNG

1. Vue d'ensemble

JUnit et TestNG sont sans aucun doute les deux frameworks de tests unitaires les plus populaires de l'écosystème Java. Bien que JUnit inspire lui-même TestNG, il fournit ses caractéristiques distinctives et, contrairement à JUnit, il fonctionne pour des niveaux de test fonctionnels et plus élevés.

Dans cet article, nous discuterons et comparerons ces frameworks en couvrant leurs fonctionnalités et leurs cas d'utilisation courants .

2. Configuration du test

Lors de l'écriture de cas de test, nous avons souvent besoin d'exécuter des instructions de configuration ou d'initialisation avant les exécutions de test, et aussi un nettoyage après la fin des tests. Évaluons-les dans les deux cadres.

JUnit propose une initialisation et un nettoyage à deux niveaux, avant et après chaque méthode et classe. Nous avons des annotations @BeforeEach , @AfterEach au niveau de la méthode et @BeforeAll et @AfterAll au niveau de la classe:

public class SummationServiceTest { private static List 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); } }

Notez que cet exemple utilise JUnit 5. Dans la version précédente de JUnit 4, nous aurions besoin d'utiliser les annotations @Before et @After qui sont équivalentes à @BeforeEach et @AfterEach. De même, @BeforeAll et @AfterAll sont des remplacements pour JUnit 4 @BeforeClass et @AfterClass.

Semblable à JUnit, TestNG fournit également l'initialisation et le nettoyage au niveau de la méthode et de la classe . Alors que @BeforeClass et @AfterClass restent les mêmes au niveau de la classe, les annotations au niveau de la méthode sont @ BeforeMethod et @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(); }

TestNG propose également des annotations @BeforeSuite, @AfterSuite, @BeforeGroup et @AfterGroup , pour les configurations au niveau de la suite et du groupe:

@BeforeGroups("positive_tests") public void runBeforeEachGroup() { numbers.add(1); numbers.add(2); numbers.add(3); } @AfterGroups("negative_tests") public void runAfterEachGroup() { numbers.clear(); }

Nous pouvons également utiliser @BeforeTest et @ AfterTest si nous avons besoin d'une configuration avant ou après les cas de test inclus dans lebalise dans le fichier de configuration XML TestNG:

Notez que la déclaration de la méthode @BeforeClass et @AfterClass doit être statique dans JUnit. Par comparaison, la déclaration de méthode TestNG n'a pas ces contraintes.

3. Ignorer les tests

Les deux frameworks prennent en charge l'ignorance des cas de test , bien qu'ils le fassent différemment. JUnit propose l' annotation @Ignore :

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

tandis que TestNG utilise @Test avec un paramètre «enabled» avec une valeur booléenne true ou false :

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

4. Exécution de tests ensemble

Exécuter des tests ensemble en tant que collection est possible à la fois dans JUnit et TestNG, mais ils le font de différentes manières.

Nous pouvons utiliser les annotations @RunWith, @SelectPackages et @SelectClasses pour regrouper les cas de test et les exécuter en tant que suite dans JUnit 5 . Une suite est un ensemble de cas de test que nous pouvons regrouper et exécuter en un seul test.

Si nous voulons regrouper les cas de test de différents packages à exécuter ensemble dans une suite, nous avons besoin de l' annotation @SelectPackages :

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

Si nous voulons que des classes de test spécifiques s'exécutent ensemble, JUnit 5 offre la flexibilité via @SelectClasses :

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

Auparavant, en utilisant JUnit 4 , nous avons réussi à regrouper et à exécuter plusieurs tests ensemble à l'aide de l' annotation @Suite :

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

Dans TestNG, nous pouvons regrouper les tests en utilisant un fichier XML:

Cela indique que RegistrationTest et SignInTest fonctionneront ensemble.

Outre le regroupement de classes, TestNG peut également regrouper des méthodes en utilisant l' annotation @ Test (groups = ”groupName”) :

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

Utilisons un XML pour exécuter les groupes:

Cela exécutera la méthode de test étiquetée avec la régression de groupe .

5. Exceptions de test

La fonction de test des exceptions à l'aide d'annotations est disponible à la fois dans JUnit et TestNG.

Commençons par créer une classe avec une méthode qui lève une exception:

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

Dans JUnit 5, nous pouvons utiliser l' API assertThrows pour tester les exceptions:

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

Dans JUnit 4, nous pouvons y parvenir en utilisant @Test (attendu = DivideByZeroException.class) sur l'API de test.

Et avec TestNG, nous pouvons également implémenter la même chose:

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

Cette fonctionnalité implique quelle exception est lancée à partir d'un morceau de code, cela fait partie d'un test.

6. Tests paramétrés

Les tests unitaires paramétrés sont utiles pour tester le même code dans plusieurs conditions. À l'aide de tests unitaires paramétrés, nous pouvons mettre en place une méthode de test qui obtient des données à partir d'une source de données. L'idée principale est de rendre la méthode de test unitaire réutilisable et de tester avec un ensemble d'entrées différent.

Dans JUnit 5 , nous avons l'avantage de méthodes de test consommant des arguments de données directement à partir de la source configurée. Par défaut, JUnit 5 fournit quelques annotations sources telles que:

  • @ValueSource: we can use this with an array of values of type Short, Byte, Int, Long, Float, Double, Char, and String:
@ParameterizedTest @ValueSource(strings = { "Hello", "World" }) void givenString_TestNullOrNot(String word) { assertNotNull(word); }
  • @EnumSource – passes Enum constants as parameters to the test method:
@ParameterizedTest @EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"}) void givenEnum_TestContainsOrNot(PizzaDeliveryStrategy timeUnit) { assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit)); }
  • @MethodSource – passes external methods generating streams:
static Stream wordDataProvider() { return Stream.of("foo", "bar"); } @ParameterizedTest @MethodSource("wordDataProvider") void givenMethodSource_TestInputStream(String argument) { assertNotNull(argument); }
  • @CsvSource – uses CSV values as a source for the parameters:
@ParameterizedTest @CsvSource({ "1, Car", "2, House", "3, Train" }) void givenCSVSource_TestContent(int id, String word) { assertNotNull(id); assertNotNull(word); }

Similarly, we have other sources like @CsvFileSource if we need to read a CSV file from classpath and @ArgumentSource to specify a custom, reusable ArgumentsProvider.

In JUnit 4, the test class has to be annotated with @RunWith to make it a parameterized class and @Parameter to use the denote the parameter values for unit test.

In TestNG, we can parametrize tests using @Parameter or @DataProvider annotations. While using the XML file annotate the test method with @Parameter:

@Test @Parameters({"value", "isEven"}) public void givenNumberFromXML_ifEvenCheckOK_thenCorrect(int value, boolean isEven) { Assert.assertEquals(isEven, value % 2 == 0); }

and provide the data in the XML file:

While using information in the XML file is simple and useful, in some cases, you might need to provide more complex data.

For this, we can use the @DataProvider annotation which allows us to map complex parameter types for testing methods.

Here's an example of using @DataProvider for primitive data types:

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

And @DataProvider for objects:

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

In the same way, any particular objects that are to be tested can be created and returned using data provider. It's useful when integrating with frameworks like Spring.

Notice that, in TestNG, since @DataProvider method need not be static, we can use multiple data provider methods in the same test class.

7. Test Timeout

Timed out tests means, a test case should fail if the execution is not completed within a certain specified period. Both JUnit and TestNG support timed out tests. In JUnit 5 we can write a timeout test as:

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

In JUnit 4 and TestNG we can the same test using @Test(timeout=1000)

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

8. Dependent Tests

TestNG supports dependency testing. This means in a set of test methods, if the initial test fails, then all subsequent dependent tests will be skipped, not marked as failed as in the case for JUnit.

Let's have a look at a scenario, where we need to validate email, and if it's successful, will proceed to log in:

@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. Order of Test Execution

There is no defined implicit order in which test methods will get executed in JUnit 4 or TestNG. The methods are just invoked as returned by the Java Reflection API. Since JUnit 4 it uses a more deterministic but not predictable order.

To have more control, we will annotate the test class with @FixMethodOrder annotation and mention a method sorter:

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

The MethodSorters.NAME_ASCENDING parameter sorts the methods by method name is lexicographic order. Apart from this sorter, we have MethodSorter.DEFAULT and MethodSorter.JVM as well.

While TestNG also provides a couple of ways to have control in the order of test method execution. We provide the priority parameter in the @Test annotation:

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

Notice, that priority invokes test methods based on priority but does not guarantee that tests in one level are completed before invoking the next priority level.

Sometimes while writing functional test cases in TestNG, we might have an interdependent test where the order of execution must be the same for every test run. To achieve that we should use the dependsOnMethods parameter to @Test annotation as we saw in the earlier section.

10. Custom Test Name

By default, whenever we run a test, the test class and the test method name is printed in console or IDE. JUnit 5 provides a unique feature where we can mention custom descriptive names for class and test methods using @DisplayName annotation.

This annotation doesn't provide any testing benefits but it brings easy to read and understand test results for a non-technical person too:

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

Chaque fois que nous exécutons le test, la sortie affichera le nom d'affichage au lieu du nom de la méthode.

À l'heure actuelle, dans TestNG, il n'y a aucun moyen de fournir un nom personnalisé.

11. Conclusion

JUnit et TestNG sont des outils modernes de test dans l'écosystème Java.

Dans cet article, nous avons examiné rapidement différentes manières d'écrire des tests avec chacun de ces deux frameworks de test.

L'implémentation de tous les extraits de code peut être trouvée dans TestNG et junit-5 Github project.