Les tests unitaires en Java Springboot

Pourquoi et comment écrire des tests unitaires ? Définition et implémentation dans une application Java Springboot
Solveig.jpg
Solveig LECARPENTIERMis à jour le 1 Juil 2021
tests unitaires spring boot

Les tests unitaires en Java Springboot sont-ils nécessaires ?

Les tests unitaires, comme leur nom l’indique, vont tester une unité (un composant) développée indépendamment du reste du programme. Ils permettent de vérifier si le résultat est celui attendu. Une unité testée doit être isolée, si elle dépend d’autres unités, il sera possible de mocker (simuler) ces dernières.

Il est recommandé d’écrire les tests en même temps que le code et de les rejouer à chaque ajout ou modification du code pour éviter les régressions.

Une bonne couverture de tests permet d’être sûr que les fonctionnalités développées fonctionnent bien avant la livraison, mais aussi de vérifier que le code développé sur une version précédente s’exécute toujours correctement sur la version courante.

Cela assure donc la maintenabilité de votre code entre chaque nouvelle version et met en évidence les cas de régression avant la mise en production.

Dans notre cas, nous allons vous présenter comment écrire des tests unitaires en java springboot.

Comment écrire des tests unitaires Java Springboot ?

Passons au côté pratique ! Comment écrire des tests unitaires en Java Springboot ? Nous vous détaillons ci-dessous avec un exemple concret, la procédure à suivre pour les dépendances, et pour créer une classe de tests.

Dépendances

Commençons par les dépendances utiles à l’écriture de nos tests, JUnit et PostgreSQL. JUnit est le framework utilisé pour l’écriture de tests, s’ajoute à celui-ci Mockito qui permet de simuler les dépendances des unités testées pour les isoler.

Nous utiliserons dans notre exemple testcontainers, une bibliothèque JavaLangage de développement très populaire ! qui prend en charge les tests JUnit et fournit des instances de BDD pouvant s’exécuter dans un conteneur Docker.

Ajout de JUnit et PostgreSQLMoteur de gestion de base de données libre de droit. (pour la BDD) au pom.xml

    <dependencies>  
        <dependency>  
            <groupId>org.testcontainers</groupId>  
            <artifactId>junit-jupiter</artifactId>  
            <version>1.15.3</version> 
            <scope>test</scope> 
        </dependency>   
        <dependency>  
            <groupId>org.testcontainers</groupId>  
            <artifactId>postgresql</artifactId>  
            <version>1.15.3</version>  
            <scope>test</scope> 
        </dependency>
    </dependencies>

Création d’une classe de tests

Dans l’arborescence de votre projet doit se trouver un dossier « src/test/java », à l’intérieur de celui-ci nous créons un nouveau dossier «controller», dans lequel tous les tests relatifs à nos contrôleurs seront ajoutés.

Créons une nouvelle classe «UserContollerTest» et une méthode de test.

    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.springframework.test.context.jdbc.Sql;
    import org.springframework.test.context.jdbc.SqlConfig;
    import org.springframework.test.context.jdbc.SqlGroup;
    
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    import org.springframework.web.context.WebApplicationContext;
    
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;


    @SqlGroup({  
        @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = { "classpath:datasets/integration/integration_test_before.sql"}, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)),
        @Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, scripts = {  "classpath:datasets/integration/integration_test_after.sql"}, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))})
    class UserControllerIT {
    
        private static final String ENDPOINT = "/api/v1/users/";
        
        @Autowired
        private WebApplicationContext context;
        
        private MockMvc mockMvc;
        
        @Mock
        private UserRepository userRepository;
        
        @InjectMocks
        private UserServiceImpl userService;
        
        @BeforeEach
        public void init() throws Exception {
            User user = new User();
            this.mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
        }
        
        /**
        * Test getUserByLastname()
        *
        *@throws Exception
        */
        @Test
        void getUser_whenLastnameExistInBDD_shouldReturnAUserAndHttpStatusOk() throws Exception {
            String lastname = "Dupont";
            
            mockMvc .perform(addAuthorizationBearerToken(get(new StringBuilder(ENDPOINT)
                    .append(lastname).toString())))
                    
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("lastname").value(lastname))
                    .andExpect(jsonPath("firstname").value("Eric"));
        }
        
        @AfterEach
        public void afterTest() throws Exception {
            SecurityContextHolder.clearContext();
        }
    }

Détaillons ensemble cette classe.

Annotations SQL

Elles vont nous permettre d’ajouter deux fichiers SQLLangage permettant de communiquer avec une base de données.. L’un sera exécuté avant les tests pour insérer des données de test, et l’autre, après les tests pour supprimer ces données qui ne nous serviront plus.

Attributs

  • Le ENDPOINT est le endpoint de notre API
  • WebApplicationContext pour définir le contexte web
  • MockMvc est un module de SpringTest qui permet de simplifier la création de tests Rest.
  • Le UserRepository et le UserServiceImpl sont ici à titre d’exemple, nous n’en avons pas besoin dans notre test. Ils utilisent les annotations @Mock et @InjectMocks. @Mock va permettre de simuler le UserRepository en reproduisant son comportement, il est possible aussi de le faire manuellement de cette façon : UserRepository userRepository = Mockito.mock(UserRepository.class) ;. @InjectMocks va créer l’objet UserServiceImpl et non une simulation, utile si l’on teste cette instance de classe ou que le corps d’une méthode de cette classe doit être exécuté.
    Les annotations Mockito minimisent le code et rendent ainsi les tests plus lisibles.

Méthodes

  • init() : utilise l’annotation @BeforeEach qui lancera donc cette méthode avant chaque test. On peut alors y réinitialiser nos objets, comme ici MockMvc. MockMvcBuilders appel la méthode webAppContextSetup qui va instancier le MockMvc à partir du contexte web et le builder. Il est possible de mettre l’annotation @BeforeAll pour lancer la méthode avant tous les tests.
  • Pour qu’une méthode soit reconnue comme une méthode de test, on ajoute l’annotation @Test. On remarquera sur un IDE qu’un bouton play apparait, il devient alors possible d’exécuter le test seul, ou d’exécuter l’ensemble de la classe test. La méthode perform() va envoyer la requête au serveur avec l’ajout d’un token « addAuthorizationBearerToken », le type de méthode, ici « get », et le endpoint suivi du nom du user que l’on recherche ce qui donne « /api/v1/users/Dupont». Elle retourne ensuite un objet de json.
    On accède ensuite à son contenu dans la méthode andExpect() pour vérifier son statut avec status() et ses attributs avec jsonPath(). Ainsi, nous pourrons s’assurer du bon fonctionnement de la méthode getUserByLastname() qui doit retourner un user si le nom dans le endpoint est présent en BDD.
    Remarque : un test ne doit pas être privé ni renvoyer une valeur.
  • La méthode afterTest() utilise l’annotation @AfterEach qui lancera donc cette méthode après chaque test. En exemple ici, si nous avions eu besoin dans un test d’initialiser le contexte de sécurité, il serait alors effacé à chaque fin de test. Il est possible de mettre l’annotation @AfterAll pour lancer la méthode après tous les tests.

Une fois tous les tests unitaires créés, s’ils s’exécutent correctement, il est possible ensuite d’écrire des tests d’intégration et vérifier la communication entre composants.

Les tests unitaires Java Springboot, on adopte ?

L’écriture des tests unitaires en Java Springboot, et pour n’importe quelle stack technique, a un coût pour un projet d’application. Cependant, retenez qu’une classe qui possède un ensemble complet de tests unitaires aura bien moins de chances d’avoir des effets de bords suite à des modifications dans le code lors de corrections ou évolutions, puisqu’ils permettent de s’assurer de la non-régression des fonctionnalités déjà développées. Donc finalement, on s’y retrouve côté coûts :)

Et vous, vous mettez en place des tests unitaires dans vos projets Java Springboot ?