Webinaire en vedette : Dévoilement de Parasoft C/C++test CT pour l'excellence en matière de tests continus et de conformité | Voir le séminaire

Pourquoi devriez-vous commencer à utiliser la moquerie pour les tests unitaires de démarrage de printemps dès maintenant

Portrait de Brian McGlauflin,
Le 5 juin 2023
7 min lire

L'écriture de tests JUnit pour vos applications Spring est facilitée par l'infrastructure de test offerte par Spring Framework et Spring Boot. Parcourez cet article organisé par des experts pour en savoir plus.

Le Cadre de printemps, avec Spring Boot, fournit un cadre de test utile pour écrire des tests JUnit pour vos candidatures Spring. Dans cet article, j'aborderai l'un des plus grands défis du test d'une application complexe : la gestion des dépendances.

Qu'est-ce que la moquerie signifie dans Spring Boot?

La moquerie est une technique utilisée dans les tests unitaires pour simuler le comportement d'objets réels lorsque l'unité testée a des dépendances externes. Les simulations, ou simulacres, sont utilisées à la place des objets réels. Le but de la moquerie est d'isoler et de se concentrer sur le code testé et non sur le comportement ou l'état des dépendances externes.

Pourquoi ai-je besoin de se moquer?

Soyons honnêtes. Les applications complexes ne sont pas construites à partir de zéro - elles utilisent des bibliothèques, des API et des projets ou services de base qui sont créés et gérés par quelqu'un d'autre. En tant que développeurs Spring, nous exploitons autant que possible les fonctionnalités existantes afin de pouvoir consacrer notre temps et nos efforts à ce qui nous tient à cœur: la logique métier de notre application. Nous laissons les détails aux bibliothèques, donc nos applications ont beaucoup de dépendances, indiquées en orange ci-dessous:

Graphique montrant les multiples dépendances d'un service Spring. Du contrôleur au service, puis à une base de données ou à des bibliothèques.
Un service Spring avec plusieurs dépendances

Alors, comment concentrer les tests unitaires sur mon application (contrôleur et service) si la plupart de ses fonctionnalités dépendent du comportement de ces dépendances ? Ne suis-je pas, au final, toujours en train de faire des tests d'intégration au lieu de tests unitaires ? Que se passe-t-il si j'ai besoin d'un meilleur contrôle sur le comportement de ces dépendances ou si les dépendances ne sont pas disponibles pendant les tests unitaires ?

Les avantages de se moquer

Ce dont j'ai besoin, c'est d'un moyen de isoler mon application à partir de ces dépendances, afin que je puisse concentrer mes tests unitaires sur mon code d'application. Dans certains cas, nous pourrions créer des versions de "test" spécialisées de ces dépendances. Cependant, l'utilisation d'une bibliothèque standardisée comme Mockito offre des avantages par rapport à cette approche pour plusieurs raisons :

  • Vous n'avez pas besoin d'écrire et de maintenir vous-même le code spécial « test ».
  • Les bibliothèques simulées peuvent suivre les invocations par rapport aux simulacres, fournissant une couche supplémentaire de validation.
  • Les bibliothèques standard telles que Mockito fournissent des fonctionnalités supplémentaires, telles que la simulation de méthodes statiques, de méthodes privées ou de constructeurs.
  • La connaissance d'une bibliothèque factice telle que Mockito peut être réutilisée dans tous les projets, tandis que la connaissance du code de test personnalisé ne peut pas être réutilisée.
Un graphique montrant comment un service simulé peut remplacer plusieurs dépendances. Le contrôleur va au service ou à un service simulé. Le service se connecte également à une base de données et à des bibliothèques, contrairement au service simulé.
Un service simulé remplace plusieurs dépendances

Dépendances au printemps

En général, les applications Spring divisent les fonctionnalités en Beans. Un contrôleur peut dépendre d'un bean de service, et le bean de service peut dépendre d'un EntityManager, d'une connexion JDBC ou d'un autre bean. La plupart du temps, les dépendances dont le code testé doit être isolé sont des beans. Dans un test d'intégration, il est logique que toutes les couches soient réelles. Mais pour les tests unitaires, nous devons décider quelles dépendances doivent être réelles et lesquelles doivent être des simulations.

Spring permet aux développeurs de définir et de configurer des beans en utilisant XML, Java ou une combinaison des deux pour fournir un mélange de beans simulés et réels dans votre configuration. Étant donné que les objets fictifs doivent être définis en Java, une classe de configuration doit être utilisée pour définir et configurer les beans simulés.

Comment démarrer les dépendances simulées dans un test de printemps ?

Un outil de test automatisé, comme l'assistant de test unitaire (UTA) de Parasoft Jtest, peut vous aider à créer des tests unitaires significatifs qui testent la fonctionnalité de vos applications Spring. Lorsque UTA génère un test Spring, toutes les dépendances de votre contrôleur sont configurées comme des simulations afin que chaque test prenne le contrôle de la dépendance. Lorsque le test est exécuté, UTA détecte les appels de méthode effectués sur un objet fictif pour les méthodes qui n'ont pas encore de méthode simulée configurée et recommande que ces méthodes soient simulées. Nous pouvons ensuite utiliser une solution rapide pour simuler automatiquement chaque méthode.

Voici un exemple de contrôleur qui dépend d'un PersonneService:

@Controller
@RequestMapping("/people")
public class PeopleController {
 
    @Autowired
    protected PersonService personService;

    @GetMapping
    public ModelAndView people(Model model){
   
        for (Person person : personService.getAllPeople()) {
            model.addAttribute(person.getName(), person.getAge());
        }
        return new ModelAndView("people.jsp", model.asMap());
    }
}

Et un exemple de test, généré par l'assistant de test unitaire de Parasoft Jtest:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class PeopleControllerTest {
 
    @Autowired
    PersonService personService;

    MockMvc mockMvc;
 
    // Other fields and setup
 
    @Configuration
    static class Config {
 
        // Other beans
 
        @Bean
        public PersonService getPersonService() {
            return mock(PersonService.class);
        }
    }
 
    @Test
    public void testPeople() throws Exception {
        // When
        ResultActions actions = mockMvc.perform(get("/people"));
    }
}

Ici, le test utilise une classe interne annotée avec @Configuration, qui fournit des dépendances de bean pour le contrôleur testé à l'aide de la configuration Java. Cela nous permet de nous moquer du PersonneService dans la méthode du haricot. Aucune méthode n'est encore moquée, donc lorsque j'exécute le test, je vois la recommandation suivante:

Cela signifie que le getAllPeople () méthode a été appelée sur ma moquée PersonneService, mais le test ne configure pas encore le mocking pour cette méthode. Lorsque je choisis l'option de correction rapide "Mock it", le test est mis à jour :

@Test
public void testPeople() throws Exception {
 Collection<Person> getAllPeopleResult = new ArrayList<Person>();
 doReturn(getAllPeopleResult).when(personService).getAllPeople();
 // When
 ResultActions actions = mockMvc.perform(get("/people"));

Quand je relance le test, il réussit. Je devrais quand même peupler le Collection qui est retourné par getAllPeople (), mais le défi de mettre en place mes dépendances simulées est résolu.

Notez que je pourrais déplacer la méthode générée moquant de la méthode de test dans la méthode bean de la classe Configuration. Si je fais cela, cela signifie que chaque test de la classe se moquera de la même méthode de la même manière. Laisser la méthode moqueuse dans la méthode de test signifie que la méthode peut être moquée différemment entre les différents tests.

Comment simuler les dépendances dans Spring Boot ?

Spring Boot rend la moquerie des haricots encore plus facile. Au lieu d'utiliser un @Autowired champ pour le bean dans le test et une classe de configuration qui le définit, vous pouvez simplement utiliser un champ pour le bean et l'annoter avec @MockBean. Spring Boot créera une maquette pour le bean en utilisant le cadre moqueur qu'il trouve sur le chemin de classe et l'injectera de la même manière que tout autre bean dans le conteneur peut être injecté.

Lors de la génération de tests Spring Boot avec l'assistant de test unitaire, le @MockBean la fonctionnalité est utilisée à la place de la classe Configuration.

@SpringBootTest
@AutoConfigureMockMvc
public class PeopleControllerTest {
    // Other fields and setup – no Configuration class needed!

    @MockBean
    PersonService personService;

    @Test
    public void testPeople() throws Exception {
        ...
    }
}

Configuration XML contre Java

Dans le premier exemple ci-dessus, la classe Configuration a fourni tous les beans au conteneur Spring. Vous pouvez également utiliser la configuration XML pour le test au lieu de la classe Configuration; ou vous pouvez combiner les deux. Par exemple:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/**/testContext.xml" })
public class PeopleControllerTest {
 
    @Autowired
    PersonService personService;
 
    // Other fields and setup
 
    @Configuration
    static class Config {
        @Bean
        @Primary
        public PersonService getPersonService() {
            return mock(PersonService.class);
        }
    }
 
    // Tests
}

Ici, la classe fait référence à un fichier de configuration XML dans le @ContextConfiguration annotation (non montrée ici) pour fournir la plupart des beans, qui peuvent être de vrais beans ou des beans spécifiques au test. Nous fournissons également un @Configuration classe, où PersonneService est moqué. Le @Primaire une annotation indique que même si un PersonneService bean se trouve dans la configuration XML, ce test utilisera le bean simulé du @Configuration classe à la place. Ce type de configuration peut rendre le code de test plus petit et plus facile à gérer.

Vous pouvez configurer UTA pour générer des tests en utilisant n'importe quel @ContextConfiguration les attributs dont vous avez besoin.

Méthodes statiques moqueuses

Parfois, les dépendances sont accessibles de manière statique. Par exemple, une application peut accéder à un service tiers via un appel de méthode statique :

public class ExternalPersonService {
    public static Person getPerson(int id) {
       RestTemplate restTemplate = new RestTemplate();
       try {
           return restTemplate.getForObject("http://domain.com/people/" + id, Person.class);
        } catch (RestClientException e) {
            return null;
        }
    }
}

Dans notre contrôleur:

    @GetMapping
    public ResponseEntity&amp;lt;Person&amp;gt; getPerson(@PathVariable("id") int id, Model model)
    {
        Person person = ExternalPersonService.getPerson(id);
        if (person != null) {
            return new ResponseEntity&amp;lt;Person&amp;gt;(person, HttpStatus.OK);
        }
        return new ResponseEntity&amp;lt;&amp;gt;(HttpStatus.NOT_FOUND);
    }

Dans cet exemple, notre méthode de gestionnaire utilise un appel de méthode statique pour obtenir un objet Person d'un service tiers. Lorsque nous construisons un test JUnit pour cette méthode de gestionnaire, un véritable appel HTTP serait effectué au service à chaque fois que le test est exécuté.

Au lieu de cela, nous moquons de la statique ExternalPersonService.getPerson () méthode. Cela empêche l'appel HTTP et nous permet de fournir un Personne réponse d'objet qui convient à nos besoins de test. L'assistant de test unitaire peut faciliter la simulation de méthodes statiques avec Mockito.

UTA génère un test pour la méthode du gestionnaire ci-dessus qui ressemble à ceci:

@Test
public void testGetPerson() throws Throwable {
    // When
    Int id = 1L;
    ResultActions actions = mockMvc.perform(get("/people/" + id));

    // Then
    actions.andExpect(status().isOk());
}

Lorsque nous exécuterons le test, nous verrons l'appel HTTP effectué dans l'arbre des flux UTA. Trouvons l'appel à ExternalPersonService.getPerson () et moquez-le à la place:

Capture d'écran montrant l'arborescence de flux de l'assistant de test unitaire de Parasoft Jtest

Le test est mis à jour pour simuler la méthode statique de ce test à l'aide de Mockito :

@Test
public void testGetPerson() throws Throwable {
    MockedStatic<ExternalPersonService>mocked = mockStatic(ExternalPersonService.class);
    mocks.add(mocked);
 
    Person getPersonResult = null; // UTA: default value
    mocked.when(()->ExternalPersonService.getPerson(anyInt())).thenReturn(getPersonResult);
 
    // When
    int id = 1;
    ResultActions actions = mockMvc.perform(get("/people/" + id));

    // Then
    actions.andExpect(status().isOk());

    }
 
    Set<AutoCloseable> mocks = new HashSet&amp;lt;&amp;gt;();
 
    @After
    public void closeMocks() throws Throwable {
        for (AutoCloseable mocked : mocks) {
            mocked.close();
        }
    }

Le Mockito mockStatique La méthode crée une maquette statique pour la classe, à travers laquelle des appels statiques spécifiques peuvent être configurés. Pour s'assurer que ce test n'affecte pas les autres dans la même exécution, les objets MockedStatic doivent être fermés à la fin du test, de sorte que toutes les simulations sont fermées dans le fermerMocks() méthode qui est ajoutée à la classe de test.

En utilisant UTA, nous pouvons maintenant sélectionner le getPersonResult variable et instanciez-la, de sorte que l'appel de la méthode simulée ne retourne pas nul:

    String name="";//UTA:default value
    int age=0;//UTA:default value
    Person getPersonResult=newPerson(name, age);

Lorsque nous réexécutons le test, getPersonResult est retourné de la moquéeExternalPersonService.getPerson () méthode et le test réussit.

Remarque: Dans l'arborescence des flux, vous pouvez également choisir «Ajouter un modèle de méthode mockable» pour les appels de méthode statique. Cela configure Unit Test Assistant pour toujours se moquer de ces appels de méthode statiques lors de la génération de nouveaux tests.

Conclusion

Les applications complexes ont souvent des dépendances fonctionnelles qui compliquent et limitent la capacité d'un développeur à tester unitairement son code. L'utilisation d'un cadre de simulation comme Mockito peut aider les développeurs à isoler le code testé de ces dépendances, leur permettant d'écrire de meilleurs tests unitaires plus rapidement. La solution de productivité des développeurs Java de Parasoft facilite la gestion des dépendances en configurant de nouveaux tests pour utiliser des simulations, et en trouvant des simulations de méthode manquantes lors de l'exécution et en aidant les développeurs à générer des simulations pour elles.

Améliorer les tests unitaires pour Java avec l'automatisation: les meilleures pratiques pour les développeurs Java