Découvrez comment la solution Parasoft Continuous Quality permet de contrôler et de gérer les environnements de test pour fournir des logiciels de haute qualité en toute confiance. Inscrivez-vous pour la démo >>
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.
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?
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:
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.
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 Evolution 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é.
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.
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.
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.