Découvrez comment intégrer facilement l'analyse statique, les tests unitaires et d'autres méthodes de test de logiciels C et C++ dans votre pipeline CI/CD. Inscrivez-vous pour la démo >>

Aimez encore plus les tests de ressorts avec l'assistant de simulation et de test unitaire

Par Brian McGlauflin

12 décembre 2017

7  min lire

Les Cadre de printemps (avec Spring Boot) fournit un cadre de test utile pour écrire des tests JUnit pour vos contrôleurs Spring.

Dans mon post précédent, nous avons expliqué comment créer et améliorer ces tests efficacement avec Parasoft Jtest Assistant de test unitaire. Dans cet article, je continuerai en abordant l'un des plus grands défis du test d'une application complexe: gestion des dépendances.

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:

1 Fig. 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, en fin de compte, toujours en train d'effectuer des tests d'intégration au lieu de tests unitaires? Que faire 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?

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

Ce dont j'ai besoin, c'est d'un moyen d'isoler mon application 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'êtes pas obligé d'écrire et de gérer vous-même le code de «test» spécial
  • Les bibliothèques moqueuses peuvent suivre les appels par rapport aux simulacres, offrant une couche supplémentaire de validation
  • Les bibliothèques standard telles que PowerMock fournissent des fonctionnalités supplémentaires, telles que des méthodes statiques simulées, des méthodes privées ou des constructeurs
  • La connaissance d'une bibliothèque moqueuse comme 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

2 Fig. 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 Controller peut dépendre d'un Bean Service et le Bean Service peut dépendre d'un EntityManager, d'une connexion JDBC ou d'un autre Bean. La plupart du temps, les dépendances à partir desquelles 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 simulées.

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.

Dépendances moqueuses

Lorsque UTA génère un test Spring, toutes les dépendances de votre contrôleur sont configurées comme des simulacres 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 pour lesquelles la simulation de méthode n'est pas encore 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;
 
    // 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 la moquerie 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.

Botte de printemps

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 framework de simulation qu'il trouve sur le chemin de classe, et l'injectera de la même manière que tout autre bean du conteneur peut être injecté.

Accélérez les tests unitaires des applications Spring avec Parasoft Jtest et son assistant de test unitaire

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 vs 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 3rd-party service via un appel de méthode statique:

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

 

Dans notre contrôleur:

    @GetMapping
    public ResponseEntity<Person> getPerson(@PathVariable("id") int id, Model modèle)
    {
        Person person = ExternalPersonService.getPerson(id);
        if (person != null) {
            return new ResponseEntity<Person>(person, HttpStatus.OK);
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

 

Dans cet exemple, notre méthode de gestionnaire utilise un appel de méthode statique pour obtenir un objet Person à partir d'un 3rd-un service de fête. 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 PowerMockito.

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

    @Test
    public void testGetPerson() throws Throwable {
        // When
        long 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:

 

 

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

    @Test
    public void testGetPerson() throws Throwable {
        spy(ExternalPersonService.class);
 
        Person getPersonResult = null; // UTA: default value
        doReturn(getPersonResult).when(ExternalPersonService.class, "getPerson", anyInt());
 
        // When
        int id = 0;
        ResultActions actions = mockMvc.perform(get("/people/" + id));
 
        // Then
        actions.andExpect(status().isOk());
    }

 

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 = new Person(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. L'outil de test unitaire 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.

Automatisez la création de tests Junit avec Parasoft Jtest et commencez à aimer les tests unitaires.
Demandez une démo maintenant

Par Brian McGlauflin

Brian McGlauflin est un ingénieur logiciel chez Parasoft avec une expérience dans le développement de pile complète avec Spring et Android, les tests d'API et la virtualisation de services. Il se concentre actuellement sur les tests logiciels automatisés pour les applications Java avec Parasoft Jtest.

Recevez les dernières nouvelles et ressources sur les tests de logiciels dans votre boîte de réception.