Voyons plus en détail les différents types de tests d'intégration que vous pouvez écrire et explorons quelques exemples. Côté ressources, pour vous préparer aux screencasts, vous pouvez partir de la branche p3ch2 du dépôt de code de ce cours :
git checkout -f p3ch2
Découvrez les tests d’intégration composants
Vous avez vu de nombreux tests unitaires jusqu’ici. Souvent en utilisant des mocks. Les tests d'intégration composants sont à peu près identiques, mais vous n’utiliserez peut-être pas de mock. Vous testez l’intégration des unités de code que vous avez développées et testées unitairement.
Jetons donc un coup d’œil à un exemple de ce type de test ! Nous allons créer un test d’intégration pour CalculatorService
, et nous assurer qu’il peut être lancé de manière autonome. Nous le nommons CalculationServiceIT
. Le « IT » dans le nom signifie test d’intégration (« integration test » en anglais).
Le fait de mettre « IT », ou « integration test » à la fin des tests est une convention Java. Elle permet notamment à Maven à séparer les tests unitaires, traités par le plugin Surefire, des tests d'intégration, traités par le plugin Failsafe. Sans trop entrer dans les détails du fonctionnement de Maven, sachez que la commande :
mvn package
permet de lancer la compilation, les tests unitaires, et le packaging des sources. Alors que la commande :
mvn verify
permet d'effectuer à la fois les tâches couvertes par mvn package, mais aussi les tests d'intégration en plus.
public class CalculatorServiceIT {
// Mettre en place des objets réels non mockés
private final Calculator calculator = new Calculator();
private final SolutionFormatter formatter = new SolutionFormatterImpl();
// Initialiser la classe à tester
private final CalculatorService underTest = new CalculatorServiceImpl(calculator, formatter);
@Test
public void calculatorService_shouldCalculateASolution_whenGivenACalculationModel() {
// GIVEN
// ...
// WHEN
final int result = underTest.calculate(
new CalculationModel(CalculationType.ADDITION, 1, 2)).getSolution();
// THEN
assertThat(result).isEqualTo(3);
}
}
Et voilà. Voyez-vous en quoi il diffère des tests unitaires précédents ? Voici le test unitaire pour l'addition :
@ExtendWith(MockitoExtension.class)
public class CalculatorServiceTest {
@Mock
Calculator calculator;
@Mock
SolutionFormatter solutionFormatter;
CalculatorService classUnderTest;
@BeforeEach
public void init() {
classUnderTest = new CalculatorServiceImpl(calculator, solutionFormatter);
}
@Test
public void calculate_shouldUseCalculator_forAddition() {
// GIVEN
when(calculator.add(1, 2)).thenReturn(3);
// WHEN
final int result = classUnderTest.calculate(
new CalculationModel(CalculationType.ADDITION, 1, 2)).getSolution();
// THEN
verify(calculator).add(1, 2);
assertThat(result).isEqualTo(3);
}
}
Il n’y a pas beaucoup de différences, n’est-ce pas ? Une chose que vous avez dû remarquer, c'est que nous n’avons pas utilisé de mocks aux lignes 4 et 5 !
Dans ce type de test, vous aurez peut-être un scénario principal et un scénario d'exception à gérer. Vous avez besoin de juste assez de tests pour avoir l’assurance que vos unités collaborent entre elles. Ici, la preuve que nous sommes capables de faire de l’arithmétique a déjà été établie par CalculatorTest, donc il n’y aurait pas de valeur ajoutée à répéter ce test ici.
Si cela fonctionne, vous pouvez faire confiance à vos tests unitaires existants, et avoir davantage d’assurance sur le bon fonctionnement des méthodes « add » (additionner), « substract » (soustraire), « multiply » (multiplier) et « divide » (diviser) quand elles sont appelées par d’autres classes !
Appréhendez les tests d’intégration système
Les tests d’intégration système partent d'une démarche similaire, à savoir assembler des composants entre eux, mais de manière plus large, avec plusieurs services, une configuration d'application, et des liens éventuels avec des composants extérieurs à l'application, simulés ou en production.
Parmi ces tests d'intégration système, il existe certains tests qui permettent de vérifier que votre application web sait gérer la soumission d'un formulaire, sans simuler directement les interactions de l'interface utilisateur (sinon c'est un test fonctionnel de bout en bout, que l'on verra dans des chapitres ultérieurs). C'est ce qu'on appelle test sous-cutané.
Vous pourriez aussi prouver que votre code sera capable d'interagir avec une base de données en lançant une partie de votre application, et en utilisant une base de données réelle ou en mémoire.
Écrivez un test d’intégration système pour une application Spring
Spring est le framework de référence utilisé par les développeurs Java pour construire leurs applications web. Il aide à coller toutes vos classes dans une application qui fonctionne. Les tests d’intégration système aident en testant un système en marche. Les frameworks comme Spring aident à rassembler les classes dans ce même système !
Spring Boot est assez facile à tester, et propose des outils de qualité pour les tests d’intégration système. C’est l’une des nombreuses raisons pour lesquelles nous l’utilisons souvent comme base de développement pour de nombreuses applications.
Notre application Calculator a été modifiée et est devenue une vraie application web basée sur Spring Boot, mais sans tests.
Pour démarrer l'application, il vous suffit de lancer la commande :
mvn spring-boot:run
Vous pouvez ensuite le voir dans votre navigateur à http://localhost:8080. Amusez-vous !
Et voici comment créer votre premier test d'intégration système :
J’ai nommé le test CalculationControllerSIT pour montrer clairement que c’est un test d’intégration système. Dans la vraie vie, vous trouverez souvent à la fois vos tests d’intégration composants et vos tests d’intégration système groupés ensemble dans votre projet. Avec Maven, ils seront gérés par le plugin Failsafe comme les tests d'intégration composants, car leur nom se termine par IT.
Observons les annotations que nous avons utilisées :
@WebMvcTest(controllers = {CalculatorController.class, CalculatorService.class})
@ExtendWith(SpringExtension.class)
public class CalculatorControllerSIT {
@Inject
private MockMvc mockMvc;
@MockBean
private SolutionFormatter solutionFormatter;
@MockBean
private Calculator calculator;
@Autowired
private CalculatorController calculatorControllerUnderTest;
@Test
public void givenAUser_whenRequestIsMadeToAdd_thenASolutionSouldBeShown() throws Exception {
...
}
}
@WebMvcTest
@WebMvcTest(controllers = {CalculatorController.class, CalculatorService.class})
Spring peut lancer un environnement simulé qui laisse agir les tests comme si vous aviez un serveur web fonctionnel. D’abord, utilisez l’annotation @WebMvcTest à la ligne 1 et passez-lui en argument les objets réels (avec Spring, on dit souvent "bean") qui seront initialisés par Spring.
Dans ce cas, nous allons utiliser un CalculatorService réel et un CalculatorController réel. Dans une application web, la classe contrôleur possède des méthodes qui dialoguent directement avec le monde extérieur à l'origine d'une requête HTTP. Dans notre code, il prend connaissance de ce que vous vouliez calculer, puis donne une réponse à votre navigateur web en HTML. Étant donné qu’il s’agit de la classe au bord de notre système à laquelle notre navigateur parle, c’est là que nous allons concentrer nos tests d’intégration système.
@ExtendWith(SpringExtension.class)
À la ligne 2, vous pouvez voir l’annotation @ExtendWith :
@ExtendWith(SpringExtension.class)
Comme avec Mockito et son extension MockitoExtension, SpringExtension assiste JUnit pour configurer une application de test, configuré avec Spring. Cette annotation est nécessaire pour activer toutes les autres annotations que nous voyons dans ce code.
@Inject
Sans entrer dans les détails de Spring, ce dernier effectue ce que l'on appelle de l'injection de dépendances (DIP en anglais). Si votre classe est un service qui a besoin d'autres composants, Spring s'occupe de les initialiser pour vous ! Concrètement, vous ne faites plus de new Calculator()
. Moins vous avez de code de routine à écrire dans vos tests, et plus vous pouvez vous concentrer sur le fait de tester les méthodes publiques et vous assurer qu’elles font ce qu’elles sont censées faire !
@Inject indique à Spring de vous donner une instance d’une classe et de gérer sa création, comme dans les lignes 5, 14 et 17 ci-dessus. Par exemple, les lignes ci-dessous donnent une instance réelle d’un CalculatorService prête à être utilisée dans vos tests :
@Inject
private CalculatorService calculatorService;
Nous nommons ces instances créées par Spring des Spring beans. Le terme bean(ou « haricot », en anglais) est très utilisé en Java ; ne le confondez pas avec ses nombreuses autres significations. Dans ce cas, c’est simplement une classe que Spring a créée et configurée pour vous, pour que vous n’ayez pas à le faire.
Pour que Spring puisse créer une classe pour vous, elle doit habituellement être désignée avec @Service, @Component, @Controller, @Bean ou encore @Named, la plus générique. Voici par exemple le code de CalculatorServiceImpl :
import javax.inject.Named;
...
@Named
public class CalculatorServiceImpl implements CalculatorService {
...
}
Regardez par vous-même le nouveau code des autres classes CalculatorController et Calculator pour découvrir ces nouvelles annotations.
@MockBean
Aux lignes 8 et 11, nous avons utilisé l’annotation @MockBean avant les attributs de type Calculator et SolutionFormatter. Le fait de placer @MockBean avant un champ demande à l’exécuteur de test de Spring de nous créer un mock.
@MockBean
private SolutionFormatter solutionFormatter;
@MockBean
private Calculator calculator;
Avec ces déclarations, nous obtenons une instance simulée du Calculator à utiliser au sein de ce test. C’est la même chose que le @Mock de Mockito, sauf que Spring utilise ces objets comme des beans Spring utilisables par d'autres beans Spring (classes annotés @Named). Par exemple, l'attribut de type CalculatorService à la ligne 14 nécessite qu’une instance de calculateur soit passée à son constructeur. Spring lui injectera le mock de calculateur que nous avons créé ici.
Dans notre exemple, CalculatorService est une instance réelle, mais il se trouve qu’il utilise le mock de calculateur que nous avons créé ci-dessus. Le fait de désigner le constructeur de CalculatorService avec l’annotation @Autowired demande à Spring de regarder les arguments qui lui sont passés :
@Autowired
public CalculatorService(Calculator calculator, SolutionFormatter formatter) {
this.calculator = calculator;
this.formatter = formatter;
}
En gros, Spring trouvera alors une classe d’un type approprié et la mettra en place. Dans un test d’intégration Spring, en désignant un attribut comme un MockBean, il devient éligible pour satisfaire à toute dépendance @Autowired dans le système sous test.
@Autowired MockMvc mockMvc
À la ligne 6, Spring nous permet d’injecter une classe spéciale nommée MockMvc dans notre test. Cela se fait exactement comme dans les exemples ci-dessus, mais nous donne également un navigateur web spécial basé sur du code que nous pouvons utiliser pour tester une application Spring sans la démarrer. La classe MockMvc appellera votre contrôleur comme si l’application avait réellement été démarrée, et vous permet d’inspecter la façon dont elle répond dans vos tests.
Jetez un coup d’œil au test !
@Test
public void givenAUser_whenRequestIsMadeToAdd_thenASolutionSouldBeShown() throws Exception {
when(calculator.add(2,3)).thenReturn(5);
mockMvc.perform(post("/calculator")
.param("leftArgument", "2")
.param("rightArgument", "3")
.param("calculationType", "ADDITION")
).andExpect(status().is2xxSuccessful()).
andExpect(content().string(containsString("id=\"solution\""))).
andExpect(content().string(containsString(">5</span>")));
verify(calculator).add(2, 3);
verify(solutionFormatter).format(5);
}
Dans ce cas, la ligne 4 utilise le faux navigateur MockMvc pour faire semblant de soumettre un formulaire. param("leftArgument", 2)
et les trois lignes suivantes soumettent des champs de formulaire, comme si quelqu’un l’avait fait dans un navigateur.
Nous avons utilisé la méthode statique post à la ligne 4 pour ce faire. La valeur "/calculator" devrait correspondre à une autre annotation dans CalculatorController désignée par @PostMapping. Voici le code réel avec lequel un navigateur interagirait :
@PostMapping("/calculator")
public String calculate(@Valid Calculation calculation, BindingResult bindingResult, Model model) {
// Arrange
CalculationType type = CalculationType.valueOf(calculation.getCalculationType());
CalculationModel calculationModel = new CalculationModel(
type,
calculation.getLeftArgument(),
calculation.getRightArgument());
// Act
CalculationModel response = calculatorService.calculate(calculationModel);
// Present
model.addAttribute("response", response);
return CALCULATOR_TEMPLATE;
}
Lorsqu’un test appelle mockMvc.perform(post("/calculator"))
, il essaye de se connecter à l’une des méthodes de votre contrôleur. La méthode post() ressemble à l’action de cliquer sur le bouton « envoyer » dans un formulaire ou un navigateur. La valeur que vous lui donnez s’appelle un chemin, et devrait correspondre à une méthode annotée dans votre contrôleur avec @PostMapping et ce chemin. Elle correspond à @PostMapping("/calculator")
à la ligne 1, ci-dessus.
Pour d’autres types de scénarios, vous pouvez avoir @GetMapping avec la méthode statique get(). Et @PutMapping avec la méthode statique put().
Vous pouvez constater que notre contrôleur ressemble aux cas de tests que nous avons écrits précédemment qui appellent CalulationService.calculate(calculationModel)
, comme nous l’avons fait à la ligne 10. Cela retourne un autre CalculationModel avec une solution. Spring utilise ensuite ce modèle pour montrer une réponse à l’utilisateur. Le code HTML se trouve dans resources/templates/calculator.html.
Pensez-y comme à une page web utilisée pour récupérer des inputs de l’utilisateur et lui montrer le résultat. Le fichier contient l’extrait suivant, qui montre la solution uniquement si une réponse a été calculée à la ligne 2. Elle remplacerait le mot solution à la ligne 4.
<div class="col">
<ul th:if="${response}">
<span id="solution" th:text="${response.getSolution()}" class="badge">
Solution
</span>
</ul>
</div>
Il s'agit d'un modèle de page HTML écrit avec Thymeleaf, configuré avec Spring Boot. Mais revenons à notre test. Les lignes 8 à 11 garantissent qu’une bonne réponse serait renvoyée à notre faux navigateur, et qu’elle contiendrait l’extrait ci-dessus avec la solution à 2+3 quand nous avons un span avec l’id de solution.
Nous avons accompli tout cela sans lancer l’application !
En résumé
Les tests d’intégration composants valident que les unités de code déjà testées unitairement collaborent réellement comme leurs mocks le faisaient.
Les tests d’intégration système sont en fait simplement des tests d’intégration composants à plus grande échelle, qui testent au sein d’un système partiellement en marche et examinent les intégrations avec d’autres collaborateurs au niveau du système (comme les bases de données).
Nous avons écrit des tests d’intégration système qui ont validé une application web Spring sans navigateur, en utilisant @WebMvcTest et l’extension JUnit SpringExtension. Cela suppose d'avoir préparé le code de l'application pour qu'il lance une application web construite avec Spring Boot.
Maintenant que vous savez implémenter vos premiers tests d'intégration, vous pourriez appliquer ces connaissances pour les tests d'acceptation ! Pour en savoir plus, rendez-vous... au prochain chapitre !