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. Tandis que JUnit inspire lui-même TestNG, il fournit ses fonctionnalités distinctives et contrairement à JUnit, il fonctionne pour des niveaux de test fonctionnels et plus élevés.

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

2. Configuration de test

Lors de l’écriture de scénarios de test, nous devons souvent exécuter certaines instructions de configuration ou d’initialisation avant l’exécution de tests, ainsi qu’un certain nettoyage une fois les tests terminés. Évaluons-les dans les deux cadres.

  • JUnit propose l’initialisation et le nettoyage à deux niveaux, avant et après chaque méthode et classe. ** Nous avons @ BeforeEach , @ AfterEach annotations au niveau de la méthode et @ BeforeAll et @ AfterAll au niveau de la 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);
    }
}

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

Semblable à JUnit, TestNG fournit également une initialisation et un 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 les annotations @ BeforeSuite, @AfterSuite, @BeforeGroup et @AfterGroup , pour les configurations aux niveaux 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();
}

De plus, nous pouvons utiliser @ BeforeTest et @ AfterTest si nous avons besoin d’une configuration avant ou après les cas de test inclus dans la balise <test> dans le fichier de configuration XML TestNG:

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

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

3. Ignorer les tests

  • Les deux frameworks supportent l’ignorance des cas de test ** , bien qu’ils le fassent très 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écuter des tests ensemble

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

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

Si nous souhaitons regrouper des cas de test de différents packages afin de les 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 soient exécutées ensemble, JUnit 5 offre la flexibilité grâce à @ 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 __ @ Suite __annotation:

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

}
  • Dans TestNG, nous pouvons grouper des tests en utilisant un fichier XML: **

<suite name="suite">
    <test name="test suite">
        <classes>
            <class name="com.baeldung.RegistrationTest"/>
            <class name="com.baeldung.SignInTest"/>
        </classes>
    </test>
</suite>

Cela indique que RegistrationTest et SignInTest seront exécutés ensemble.

Outre le regroupement de classes, TestNG peut également grouper des méthodes à l’aide de l’annotation @ Test (groups = "nomGroupe")

@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:

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

Ceci exécutera la méthode de test étiquetée avec le groupe regression .

5. Tester les exceptions

  • La fonctionnalité permettant de tester les 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 __assertThrows __API pour tester les exceptions:

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

Dans _JUnit 4, we peut 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 émise par un morceau de code faisant 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 configurer une méthode de test permettant d’obtenir 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 pouvoir utiliser des méthodes de test utilisant des arguments de données directement à partir de la source configurée. ** Par défaut, JUnit 5 fournit quelques annotations de source telles que:

  • @ ValueSource: nous pouvons l’utiliser avec un tableau de valeurs de type

Short, Byte, Int, Long, Float, Double, Char, et Chaîne:

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void givenString__TestNullOrNot(String word) {
    assertNotNull(word);
}
  • __ @ EnumSource - passe Enum __constants en tant que paramètres du test

méthode:

@ParameterizedTest
@EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"})
void givenEnum__TestContainsOrNot(PizzaDeliveryStrategy timeUnit) {
    assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit));
}
  • __ @ MethodSource - p __asse les méthodes externes générant des flux:

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

@ParameterizedTest
@MethodSource("wordDataProvider")
void givenMethodSource__TestInputStream(String argument) {
    assertNotNull(argument);
}
  • @ CsvSource – utilise les valeurs CSV comme source pour les paramètres:

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

De même, nous avons d’autres sources telles que _ @ CsvFileSource if si nous devons lire un fichier CSV à partir de classpath et @ ArgumentSource pour spécifier un ArgumentsProvider._ personnalisé et réutilisable

Dans JUnit 4 , la classe de test doit être annotée avec @ RunWith pour en faire une classe paramétrée et @ Parameter pour utiliser les valeurs de paramètre désignant le test unitaire.

  • Dans TestNG, nous pouvons paramétrer des tests à l’aide d’annotations @ Parameter ou @ DataProvider . ** Lors de l’utilisation du fichier XML, annotez la méthode de test avec @ Parameter:

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

et fournissez les données dans le fichier XML:

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

Bien que l’utilisation des informations dans le fichier XML soit simple et utile, dans certains cas, il peut être nécessaire de fournir des données plus complexes.

Pour cela, nous pouvons utiliser l’annotation @ DataProvider qui nous permet de mapper des types de paramètres complexes pour les méthodes de test.

Voici un exemple d’utilisation de @ DataProvider pour les types de données primitifs:

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

Et __ @ DataProvider __for objets:

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

De la même manière, tous les objets particuliers à tester peuvent être créés et renvoyés à l’aide du fournisseur de données. C’est utile lors de l’intégration avec des frameworks comme Spring.

Notez que, dans TestNG, étant donné que la méthode @ DataProvider n’est pas nécessairement statique, nous pouvons utiliser plusieurs méthodes de fournisseur de données dans la même classe de test.

7. Test Timeout

Tests expirés signifie qu’un scénario de test doit échouer si l’exécution n’est pas terminée dans un délai spécifié. JUnit et TestNG ont tous deux pris en charge les tests ayant expiré. Dans JUnit 5 , nous pouvons écrire un test de dépassement de délai sous la forme _: _

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

Dans JUnit 4 et TestNG, nous pouvons effectuer le même test avec @ Test (timeout = 1000)

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

8. Tests dépendants

TestNG prend en charge les tests de dépendance. Cela signifie que dans un ensemble de méthodes de test, si le test initial échoue, tous les tests dépendants ultérieurs seront ignorés et ne seront pas marqués comme ayant échoué, comme dans le cas de JUnit.

Examinons un scénario dans lequel nous devons valider le courrier électronique et s’il réussit, nous allons procéder à la connexion:

@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. Ordre d’exécution du test

  • Il n’y a pas d’ordre implicite défini dans lequel les méthodes de test seront exécutées dans JUnit 4 ou TestNG. ** Les méthodes sont simplement appelées comme renvoyées par l’API Java Reflection. Depuis JUnit 4, il utilise un ordre plus déterministe mais non prévisible.

Pour avoir plus de contrôle, nous annoterons la classe de test avec l’annotation @ FixMethodOrder et mentionnerons un trieur de méthodes:

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

}

Le paramètre MethodSorters.NAME ASCENDING trie les méthodes par nom de méthode dans l’ordre lexicographique. Outre cette trieuse, nous avons également MethodSorter.DEFAULT et MethodSorter.JVM.__

Alors que TestNG fournit également deux façons d’avoir le contrôle dans l’ordre d’exécution des méthodes de test. Nous fournissons le paramètre priority dans l’annotation @ 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);
}

Notez que cette priorité appelle des méthodes de test basées sur la priorité mais ne garantit pas que les tests d’un niveau sont terminés avant d’appeler le niveau de priorité suivant.

Parfois, lors de l’écriture de cas de test fonctionnels dans TestNG, nous pouvons avoir un test interdépendant dans lequel l’ordre d’exécution doit être le même pour chaque exécution de test. Pour cela, nous devrions utiliser le paramètre dependsOnMethods dans l’annotation @ Test , comme nous l’avons vu dans la section précédente.

10. Nom du test personnalisé

Par défaut, chaque fois que nous exécutons un test, la classe de test et le nom de la méthode de test sont imprimés dans la console ou dans l’EDI. JUnit 5 fournit une fonctionnalité unique dans laquelle nous pouvons mentionner des noms descriptifs personnalisés pour les méthodes de classe et de test utilisant l’annotation @ DisplayName .

Cette annotation ne présente aucun avantage en matière de test, mais apporte également une lecture et une compréhension faciles des résultats de test à une personne non technique:

@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 affiche le nom complet au lieu du nom de la méthode.

Pour l’instant, dans TestNG , il n’existe 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 brièvement passé en revue différentes manières d’écrire des tests avec chacun de ces deux frameworks de test.

La mise en œuvre de tous les extraits de code est disponible dans les TestNG et projet Github de junit-5 .