Archives Wild Code School
Java

Java and Testing with JUnit5

Quête 19 sur 43 · 1 image archivée

Introduction

Les tests sont essentiels dans le développement logiciel professionnel d'aujourd'hui. L'outil de référence en Java est le JUnit Framework (framework de tests, c'est-à-dire une boîte à outils qui fournit la structure pour écrire et lancer tes tests). La version actuelle est JUnit5, mais ne sois pas surprise de tomber sur des projets qui utilisent des versions plus anciennes de JUnit. Certains projets utilisent un framework de tests unitaires différent, par exemple TestNG.

Inspection

Quand tu développes un logiciel, le code n'est qu'une partie du travail. Comprendre le domaine du problème et écrire les besoins en est une autre, et enfin le test offre un moyen de vérifier que le comportement attendu par la spécification correspond bien au comportement réel du code.

🤓 À la fin de cette quête, tu sauras :

✅ Pourquoi tu as besoin de tests dans le développement logiciel professionnel ✅ Comment écrire et lancer des tests avec JUnit5 et Java


Prérequis

Pour utiliser JUnit5, tu dois inclure la dépendance JUnit5 dans ton projet Java. Les coordonnées Maven pour JUnit5 sont présentées dans l'extrait de pom.xml suivant. Tu dois aussi inclure quelques plugins pour que Maven lance tes tests lorsque tu démarres un build (la construction du projet, c'est-à-dire l'étape où Maven compile et assemble ton application).

<dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>5.8.1</version>
                <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
                <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

Dans l'ensemble, l'utilisation de JUnit5 ne diffère pas de celle de n'importe quelle autre bibliothèque Java disponible avec Maven. La plus grande différence est le scope (la portée, c'est-à-dire à quel moment et dans quel contexte la dépendance est utilisée), qui doit être réglé sur test pour JUnit5. Si tu ne précises pas le scope, JUnit5 sera inclus avec ton logiciel, ce qui n'est pas ce que tu veux.

Un premier test

C'est une bonne pratique d'écrire les tests au niveau de la class. Pour obtenir une couverture de classes à 100 %, tu devrais avoir une classe de test pour chaque classe. Pour un premier test, il faut écrire une classe que l'on veut tester.

java

public class Person {

    private String firstname;
    private String lastname;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {

        this.lastname = lastname;
    }
}

La classe de test doit se trouver dans le même package que la classe à tester. Dans un projet Maven, le fichier source de la classe à tester se situe dans le dossier src/main/java. Le fichier source de la classe de test doit, lui, se situer dans le dossier src/test/java.

java
import org.junit.jupiter.api.Test;

class PersonTest {

    private Person cut;

    @BeforeEach
    void setup() {
        cut = new Person();
    }


    @Test
    void setFirstname_test() {
        // ...
    }

    // ...

    @AfterEach
    void teardown() {
        cut = null;
    }
}

En règle générale, tu devrais éviter les méthodes static. Si tu utilises des méthodes static pour des besoins précis, tu ne devrais pas mélanger méthodes static et non-static dans une même classe. Pour la plupart des classes, tu ne fourniras que des méthodes non-static. Pour utiliser une méthode non-static, il faut d'abord créer une instance de la classe que tu veux tester.

La variable cut sert à stocker la référence vers l'instance de la classe que l'on crée pour le test, où cut est juste une abréviation de class under test (classe sous test, c'est-à-dire la classe qui est en train d'être testée).

Pour tester n'importe quelle méthode de cette classe, il faut d'abord créer une instance de la classe à tester. C'est une étape de préparation (setup) qui doit être exécutée avant que la méthode de test elle-même ne soit exécutée. Cette préparation doit être mise en place dans une méthode setup(), et cette méthode setup doit être annotée avec l'annotation @BeforeEach pour que JUnit exécute cette méthode avant chaque méthode de test.

La méthode test() est la véritable méthode de test. Cette méthode n'a pas encore d'implémentation, mais JUnit5 l'exécutera quand même comme un test, car elle est annotée avec l'annotation @Test. Nous ajouterons l'implémentation plus tard.

La dernière méthode de l'exemple s'appelle teardown() et sert à faire un peu de rangement après l'exécution du test. Cette méthode est exécutée après chaque test parce qu'elle est annotée avec l'annotation @AfterEach.

Isoler les tests les uns des autres est un facteur important à prendre en compte quand on écrit des tests. Des tests isolés évitent les effets de bord et, parfois, l'isolement lui-même aide beaucoup à identifier des problèmes existants.

Tester quelque chose

À l'intérieur de chaque méthode de test, on a généralement trois tâches.

D'abord, il faut arranger (arrange) les choses pour qu'il soit possible d'appeler la méthode que l'on veut tester. Toute forme de préparation appartient à cette section.

La deuxième étape devrait être l'appel de la méthode à tester. Le résultat de l'appel est stocké dans une variable.

La dernière étape est la vérification du résultat attendu par rapport au résultat obtenu. Tu devrais toujours écrire le résultat attendu comme premier paramètre.

Info

Certains frameworks attendent le résultat attendu et le résultat réel dans l'ordre inverse. Même si ce n'est pas un problème critique, cela peut mener à des messages trompeurs sur la raison de l'échec du test.

java
import org.junit.jupiter.api.Test;

    @Test
    void getFirstname_test() {
        // Arrange
        cut.setFirstname("Sarah");

        // Act
        var result = cut.getFirstname();

        // Assert
        assertEquals("Sarah", result);
    }

Dans cet exemple, on utilise la méthode assertEqual des assertions JUnit5 pour vérifier que le résultat attendu correspond bien au résultat réel renvoyé par un appel à la méthode que l'on teste.

Assertions

Les assertions (affirmations, c'est-à-dire des vérifications qui confirment que le résultat attendu correspond au résultat obtenu) offrent un moyen d'affirmer que le résultat attendu correspond au résultat réel. Chaque assertion sera toujours vraie ou fausse. La différence entre les assertions se situe dans les paramètres et, parfois, dans la logique derrière l'assertion.

JUnit5 fournit des méthodes pour correspondre à plusieurs types de données. Ça vaut vraiment le coup de regarder toutes les méthodes une fois. Il y a quelques différences notables que tu devrais connaître.

assertTrue et assertFalse

Pour ces méthodes, tu n'as pas besoin de préciser le résultat attendu comme paramètre. Le résultat attendu est déduit du nom de la méthode.

assertEqual et assertNotEqual

La méthode assertEqual et la méthode assertNotEqual vérifient l'égalité du résultat réel et du résultat attendu. Les deux méthodes utilisent la méthode equals pour vérifier l'égalité. Si tu as zappé l'implémentation de méthodes equals correctes dans tes classes, il faudra t'en occuper pour réussir avec JUnit5.

assertSame et assertNotSame

Une simple égalité basée sur la référence d'une instance d'objet est fournie par les méthodes assertSame et assertNotSame. Ces méthodes utilisent l'opérateur == pour comparer le résultat réel et le résultat attendu.

assertNull et assertNotNull

Même si assertNull n'est mentionnée que maintenant, c'est une assertion importante. Quand une méthode renvoie une référence vers une instance d'objet, tu devrais utiliser la méthode assertNotNull pour vérifier que le résultat n'est pas la valeur null. Si tu loupes ça, tu risques d'obtenir une NullPointerException dans des assertions ultérieures, car celles-ci essaient d'accéder à une instance d'objet avec une référence qui a une valeur null.

JUnit5 prend en charge bien d'autres assertions. Jette un œil à la documentation de JUnit5 pour une liste complète.

JUnit5 fournit un message par défaut quand une assertion échoue. Tu peux aussi ajouter un paramètre supplémentaire pour préciser ton propre message. ...

Référence

↗
https://junit.org/junit5/docs/current/user-guide/

La référence définitive sur JUnit5 est la documentation JUnit5 elle-même.

junit.org

Écrire des tests unitaires fait partie d'un processus de développement logiciel professionnel. Comme tu devrais t'en rendre compte, écrire des tests prend un temps considérable. Pourtant, dans beaucoup de projets logiciels, la majorité du travail est consacrée au code normal. Les tests logiciels te protègent des effets de bord indésirables du développement quand tu maintiens du code que tu n'as pas écrit toi-même au départ. Même quand tu as écrit le code, c'est une bonne idée de faire écrire les tests par quelqu'un d'autre, car il est naturel de ne pas voir les défauts évidents dans son propre code.

☝️ Résumé

Cette quête a présenté les besoins de base pour écrire des tests avec JUnit5. Écrire des tests représente beaucoup de travail qui peut sembler inutile. Pour le développement logiciel professionnel, le code doit être testé. On a jeté un œil à des sujets comme la couverture de code (code coverage) à différents niveaux pour avoir une idée du temps nécessaire à l'implémentation de tous les tests.


💪Challenge

Un bon challenge sur les tests unitaires nécessite du code à tester. Pour ce challenge, on va utiliser la class List développée dans la quête Java précédente. Utilise l'application Java complète, y compris la classe list, et étends ce projet avec des tests unitaires. Il n'est pas nécessaire de changer quoi que ce soit dans le code de l'application elle-même.

Le challenge de cette quête est de créer des tests pour la quête précédente. Dans la quête précédente, on a créé une classe List. Tes tests doivent, au minimum, couvrir cette classe. Tes tests doivent aussi couvrir une bonne partie des méthodes de la classe list.

Cette tâche est limitée dans le temps (time boxed). Écris simplement autant de tests que tu peux, mais tu dois nous parler de ta

  • couverture de classes pour le projet
  • couverture de méthodes pour la classe list

Pour au moins une méthode que tu testes, tu dois nous parler des différents cas de test que tes tests couvrent.

Échange ton code pour les tests avec un membre de l'équipe afin de mettre en place un principe des quatre yeux (four-eyes principle).

🧐 Critères d'acceptation

  • ✔ Tu as une couverture de classes d'au moins 20 %
  • ✔ Tu as une couverture de méthodes d'au moins 50 % pour la classe list
  • ✔ Tu inclus un fichier texte qui décrit la couverture de test comme spécifié ci-dessus
  • ✔ Ton code est poussé sur le serveur de dépôt git (GitHub, GitLab, ...)

Introduction

Tests are essential in today's professional software development. The tool of choice in Java is the JUnit Framework. The current version of JUnit is JUnit5 but do not be surprised to find projects that use older versions of JUnit. Some projects use a different unit testing framework, for example TestNG.

Inspection

When you develop software, code is only one part of the work. Understanding the problem domain and write down the requirements is another part of the work and finally the test provides a way to verify that the expected behavior from the specification matches the actual behavior of the code.

🤓 By the end of this quest, you will know:

✅ Why you need to tests in professional software development ✅ How to write and run tests with JUnit5 and Java


Prerequisites

To use JUnit5, you have to include the JUnit5 dependency into your Java project. The maven coordinates for JUnit5 are presented in the following pom.xml snippet. You also have to include some plugins to make maven run your tests when you start a build.

<dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>5.8.1</version>
                <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
                <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

For the most part, the use of JUnit5 does not differ from using any other Java library available with Maven. The biggest difference is the scope, that should be set to test for JUnit5. When you do not specify the scope, JUnit5 will be included with your software which is not what you want.

A first test

It is good practice to write tests on class level. To get a 100% coverage of classes, you should have a test class for each class. For a first test, we have to write a class that we want to test.

java

public class Person {

    private String firstname;
    private String lastname;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {

        this.lastname = lastname;
    }
}

The test class should be located in the same package as the class to test. Within a maven project, the source file for the class to test is located in the src/main/java folder. The source file for the test class should be located in the src/test/java folder.

java
import org.junit.jupiter.api.Test;

class PersonTest {

    private Person cut;

    @BeforeEach
    void setup() {
        cut = new Person();
    }


    @Test
    void setFirstname_test() {
        // ...
    }

    // ...

    @AfterEach
    void teardown() {
        cut = null;
    }
}

As a rule of thumb, you should avoid static methods. If you use static methods for specific purposes, you should not mix static and non-static methods in a class. For most classes, you will only provide non-static methods. To use a non-static method, you have to create an instance of the class that you want to test first.

The variable cut is used to store the reference to the instance of the class we create for the test, where cut is just an abbreviation for class under test.

To test any method on that class, you have to create an instance for the class to test first. This is a setup step that should be executed before the test method itself is executed. This setup should be implemented in a setup() method and the setup method has to be annotated with the @BeforeEach annotation to make JUnit execute this setup method before each test method.

The method test() is the actual test method. This method has no implementation, yet, but JUnit5 will execute the method as a test nonetheless since it is annotated with the @Test annotation. We will add the implementation later.

The last method in the example is named teardown() and is a place to do some housekeeping after test execution. This method is executed after each test because it is annotated with the @AfterEach annotation.

Isolating tests from each other is an important factor to take into account when writing tests. Isolated tests will avoid side effects and sometimes isolation itself helps a lot to identify existing problems.

Test something

Within each test method, we usually have three tasks.

First of all, we have to arrange that it is possible to call the method we want to test. Any kind of preparation belongs in this section.

The second step should be the call of the method to test. The result from the call is stored in a variable.

The last step is the verification of the expected outcome with the result. You should always write the expected outcome as the first parameter.

Info

Some frameworks expect the expected result and the actual result in the opposite order. While this is not a critical issue, it can lead to misleading messages why the test failed.

java
import org.junit.jupiter.api.Test;

    @Test
    void getFirstname_test() {
        // Arrange
        cut.setFirstname("Sarah");

        // Act
        var result = cut.getFirstname();

        // Assert
        assertEquals("Sarah", result);
    }

In this example, we use the assertEqual method from the JUnit5 assertions to verify that the expected result matches the actual result from a call to the method we test.

Assertions

Assertions provide a way to assert that the expected result matches with the actual result. Every assertion will always be true or false. The difference between the assertions is the parameters and sometimes, the logic behind the assertion.

JUnit5 provides methods to match several datatypes. It is really worth to look up all methods once. There are some notable differences that you should know about.

assertTrue and assertFalse

For these methods, you do not have to specify the expected result as a parameter. The expected result is derived from the method name.

assertEqual and assertNotEqual

The assertEqual method and the assertNotEqual method assert the equality of actual and expected result. Both methods will use the equals method to verify equality. If you missed out on implementing proper equals methods in your classes, you have to follow up to be successful with JUnit5.

assertSame and assertNotSame

A simple equality based on the reference for an object instance is provided by the assertSame and assertNotSame methods. These methods will use the == operator to compare actual and expected result

assertNull and assertNotNull

While assertNull is mentioned only now, it is an important assertion. When a method returns a reference to an object instance, you should use the assertNotNull method to verify that the result is not the null value. If you miss on this, you may get a NullPointerException in later assertions because these try to access an object instance with a reference that has a null value.

JUnit5 supports many more assertions. Have a look at the documentation for JUnit5 for a complete list.

JUnit5 provides a default message when an assertion fails. You can also add an additional parameter to specify your own message. ...

Reference

↗
https://junit.org/junit5/docs/current/user-guide/

The definitive reference on JUnit5 is the JUnit5 documentation itself.

junit.org

Writing unit tests is part of a professional software development process. As you should realize, writing tests takes a considerable amount of time. Yet, in many software projects, the majority of work is put into normal code. Software tests shield against unwanted side effects from software development when you are maintaining code that you did not write in the first place. Even when you wrote the code, it is a good idea to have someone else write the tests because it is natural not see the obvious flaws in your own code.

☝️ Summary

This quest introduced the basic requirements to write tests with JUnit5. Writing tests is a lot of work that may seem unnecessary. For professional software development, code has to be tested. We had a peek into topics as code coverage on different levels to get an idea about the time required to implement all the tests.


💪Challenge

A good challenge about unit testing requires some code that is tested. For this challenge, we will use the List class that was developed in the previous Java quest. Use the complete Java application including the list class and extend this project with unit tests. It is not required to change anything in the code for the Application itself.

The challenge for this quest is to create tests for the previous quest. In the previous quest we create a List class. Your tests should, at least, cover this class. Your tests should also cover a fair amount of methods from the list class.

This task is time boxed. Just write as many tests as you can but you have to tell us about your

  • class coverage for the project
  • method coverage for the list class

For at least one method you test, you have to tell us about the different test cases your tests cover.

Swap the code for the tests with a team member to implement a four-eyes principle.

🧐 Acceptance criteria

  • ✔ You have a class coverage of at least 20%
  • ✔ You have a method coverage of at least 50% for the list class
  • ✔ You include a text file that describes the test coverage as specified above
  • ✔ Your code is pushed to the git repository server (GitHub, GitLab, ...)

Ressources partagées sur cette quête

  • bien expliquer et court https://openclassrooms.com/fr/courses/6100311-testez-votre-code-java-pour-realiser-des-applications-de-qualite/6465561-structurez-vos-tests-unitaires-avec-les-annotations-junit