ALOM
✅ Tests
# Types de tests * Tests unitaires (TU) * Test d'intégration (TI) * Tests end-to-end (E2E) --- # La pyramide des tests ![](images/test-pyramid.png) --- ## Durée des tests * TU ~ milliseconds * TI ~ seconds / minutes * E2E ~ minutes --- ## Intérêt / Périmètre | Type | Scope | Intérêt | |------|--------------------------------------------|---------------------------------------------------| | TU | Méthode/ Classe | Boucle de feedback rapide | | TI | Composant | Intégration avec d'autres composants (Frameworks, BDD, APIs) | | E2E | Application | Tests complets (comportement, non-régression, performance, sécurité) | --- ## Périmètre des tests - TU Une classe ou un composant uniquement. ![](images/scope-tu.png) --- ## Périmètre des tests - TI / E2E L'application entièrement démarrée, ou en partie ![](images/scope-ti.png) --- ## Outillage * TU : JUnit / Mockito / AssertJ * TI : Tests Spring / TestContainers / WireMock * E2E : Cucumber / Selenium * Performance : JMeter / Gatling --- ## Structure d'un test 1. Le setup 🔧 2. L'exécution ▶️ 3. Les assertions ✅ 4. Le nettoyage 🧹 --- ## Structure d'un test ```java @Test void shouldCalculateHealthCorrectly(){ // setup 🔧 var calculator = new StatsCalculator(); var pikachu = new Pokemon("pikachu", 50); // exécution ▶️ var health = calculator.calculateHealth(pikachu); // assertions ✅ assertEquals(95, health); } ``` --- ## Structure d'un test ```java @Test void shouldCalculateHealthCorrectly(){ // setup 🔧 var calculator = new StatsCalculator(); var pikachu = new Pokemon("pikachu"); // exécution ▶️ & assertions ✅ assertThat(calculator.calculateHealth(pikachu.atLevel(6)).isEqualTo(20); assertThat(calculator.calculateHealth(pikachu.atLevel(18)).isEqualTo(40); assertThat(calculator.calculateHealth(pikachu.atLevel(50)).isEqualTo(95); assertThat(calculator.calculateHealth(pikachu.atLevel(100)).isEqualTo(180); } ``` --- ## Structure d'un test ⚠️ Un test sans assertions : * "couvre le code source" * ne valide pas son comportement > "On sait juste que ça ne plante pas" > > --
Un(e) dev qui a pas écrit d'assertion
--- # Dans ce cours * [JUnit 5](#/junit-5) * [Mockito](#/mockito) * [AssertJ](#/assertj) * [`@SpringBootTest`](#/springboottest) * [`@MockBean`](#/mockbean) * [MockMVC](#/mockmvc) * [TestContainers](#/testcontainers) * [WireMock](#/wiremock) * [Cucumber](#/cucumber) --- # JUnit 5 ([doc](https://junit.org/junit5/docs/current/user-guide/#writing-tests)) ## *junit-jupiter-api* * `@Test` : déclare une méthode de test * `@Nested` : permet de grouper des tests dans une classe * `@BeforeEach`, `@AfterEach` : méthode à exécuter avant/après chaque test * `@BeforeAll`, `@AfterAll` : méthode statique à exécuter avant/après la classe de test --- ## *junit-jupiter-api* ```java import org.junit.jupiter.api.*; class DummyTest { @BeforeAll static void beforeAll() {} @AfterAll static void afterAll() {} @BeforeEach void setUp() {} @AfterEach void tearDown() {} @Test void testMethod(){} } ``` --- # Mockito ([doc](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html])) * `Mockito.mock(Class
classToMock)` : crée un mock * `Mockito.when(T methodCall)` : ajoute du comportement à un mock (stubbing) * `Mockito.verify(T mock)` : vérifie qu'un mock a été appelé --- ## Mockito usage ```java void showPokedex_shouldListThePokemonTypes_usingTheService(){ // setup 🔧 var serviceMock = Mockito.mock(PokemonTypeService.class); var controller = new PokemonTypeController(pokemonTypeServiceMock); // exécution ▶️ controller.showPokedex(); // assertions ✅ Mockito.verify(serviceMock).listPokemonsTypes(); } ``` --- ## Mockito usage with annotations ```java @ExtendWith(MockitoExtension.class) class MockitoTest { // setup 🔧 - crée l'objet, et injecte les champs annotés `@Mock` @InjectMocks PokemonTypeController controller; // setup 🔧 - équivalent à `Mockito.mock` @Mock PokemonTypeService service; void showPokedex_shouldListThePokemonTypes_usingTheService(){ // exécution ▶️ controller.showPokedex(); // assertions ✅ Mockito.verify(serviceMock).listPokemonTypes(); } } ``` --- ## Périmètre des tests - TU ⚠️ L'objet annoté `@InjectMock` est celui qu'on teste ![](images/scope-tu.png) --- ## Mockito usage stubbing Donner du comportement aux mocks avec `when().thenReturn()` et `when().thenThrow()` ```java @ExtendWith(MockitoExtension.class) class MockitoTest { @InjectMocks PokemonTypeController controller; @Mock PokemonTypeService service; void showPokedex_shouldListThePokemonTypes_usingTheService(){ // setup 🔧 - le mock retourne une liste à un seul élément var pikachu = new Pokemon("pikachu"); when(service.listPokemonTypes()).thenReturn(List.of(pikachu)); // exécution ▶️ controller.showPokedex(); // assertions ✅ Mockito.verify(serviceMock).listPokemonTypes(); } } ``` --- ## Mockito usage verifications Donner du comportement aux mocks avec `when().thenReturn()` et `when().thenThrow()` ```java @ExtendWith(MockitoExtension.class) class MockitoTest { @InjectMocks PokemonTypeController controller; @Mock PokemonTypeService service; void showPokedex_shouldListThePokemonTypes_usingTheService(){ // setup 🔧 - le mock retourne une liste à un seul élément var pikachu = new Pokemon("pikachu"); when(service.listPokemonTypes()).thenReturn(List.of(pikachu)); // exécution ▶️ controller.showPokedex(); // assertions ✅ Mockito.verify(serviceMock).listPokemonTypes(); Mockito.verity(serviceMock, never()).getPokemon(anyInt()); } } ``` --- ## Mockito usage verifications - ArgumentMatchers Matcher les arguments pour `when()` et `verify()` ```java import static org.mockito.ArgumentMatchers.*; void matchersDemo(){ // eq() when(service.getPokemon(eq(25))).thenReturn(new Pokemon("pikachu")); // any() - anyInt() - anyBoolean() - any(T) ... when(service.getPokemon(anyInt())).thenReturn(new Pokemon("pikachu")); // something notNull() or isNull() when(service.levelUpPokemon(isNull())).thenThrow(new IllegalArgumentException()); // something that contains a string when(service.getPokemonFromType(contains("electric")).thenReturn(new Pokemon("pikachu")); // etc } ``` --- # AssertJ ([doc](https://assertj.github.io/doc/)) ## Assertions "fluent" Les assertions JUnit sont assez limitées : * `assertTrue(boolean)` / `assertFalse(boolean)` * `assertEquals(Object, Object)` * `assertNull(Object)` / assertNotNull(Object)` --- ## Assertions "fluent" ```java import static org.assertj.core.api.Assertions.*; // assertion simple ✅ assertThat(pikachu.level()).isEqualTo(14); // assertions ✅ chaînées sur un même objet assertThat(pikachu.name()) .isNotNull() .isEqualTo("Pikachu"); ``` --- # SpringBoot Tests ([doc](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)) ```xml
org.springframework.boot
spring-boot-starter-test
``` Importe : * JUnit 5 * Mockito * AssertJ --- ## @SpringBootTest Déclare un test Spring ! Démarre l'application entière (comme avec le `@SpringBootApplication`), sans la partie serveur. ```java @SpringBootTest class PokemonTypeServiceIntegrationTest { // on peut recevoir des beans Spring en injection de dépendance ! @Autowired PokemonTypeService service; @Test void doSomething(){ // tester ! } } ``` --- ## @SpringBootTest La configuration chargée est celle présente dans `src/test/resources` en priorité. Possibilité de définir des beans annotés `@TestConfiguration` pour surcharger des beans ou de la conf. --- ## @SpringBootTest ![](images/spring-boot-test-scope-controller.png) --- ## @MockBean Insère un mock Mockito dans le contexte Spring ```java @SpringBootTest class PokemonTypeControllerIT { // déclaration d'un mock bean ! @MockBean PokemonTypeAPIService service; // le contrôleur recevra le mock en injection de dépendance @Autowired PokemonTypeController controller; @Test void doSomething(){ // setup 🔧 - le mock retourne une liste à un seul élément var pikachu = new Pokemon("pikachu"); when(service.listPokemonTypes()).thenReturn(List.of(pikachu)); // exécution ▶️ controller.showPokedex(); // assertions ✅ Mockito.verify(serviceMock).listPokemonTypes(); } } ``` --- ## @MockBean ⚠️ L'utilisation de `@MockBean` occasionnera la re-création du contexte Spring à la sortie de la classe de tests ⚠️ Préférer un TU avec Mockito si possible. --- ## @MockBean ![](images/spring-boot-test-mockbean.png) --- # MockMVC Tests d'intégration de la couche Contrôleur. Permet de simuler des appels HTTP (GET/POST...) avec une API "fluent", et de faire des assertions sur le résultat (code HTTP, réponse, headers...). ```java @SpringBootTest @AutoConfigureMockMvc class MockMvcTest { @Autowired private MockMvc mockMvc; @Test void dummy(){ // test code } } ``` --- ## MockMVC ![](images/spring-boot-test-mockmvc.png) --- ## MockMVC ```java import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; var expectedJson = """ { "id": 1, "name": "Bulbizarre" } """; // Envoie une requête GET à /pokemon-types/1 mockMvc.perform(get("/pokemon-types/1")) .andExpect(status().isOk()) // Attend une réponse HTTP 200 OK .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(header().string(HttpHeaders.CONTENT_LANGUAGE, "fr-fr")) .andExpect(content().json(expectedJson)); // asserte le résultat ``` --- ## MockMVC Permet d'automatiser les tests au niveau de la couche API / MVC ⚠️ Tests parfois compliqués à écrire et maintenir, mais ça vaut le coup. 💡 Peut raisonnablement remplacer les TU/TI sur la couche Contrôleur. --- # TestContainers ([doc](https://java.testcontainers.org/)) Propose d'utiliser des containers Docker "jetables" pour exécuter les tests d'intégration. Pratique pour tester sur une vraie BDD, ou service externe (en remplacement d'un `h2` par exemple). --- ## TestContainers Supporte : * les BDD relationnelles (Oracle, MySQL, PostgreSQL) * les systèmes "NoSQL" (Cassandra, Couchbase, Elasticsearch, MongoDb, Neo4j, Redis) * des message brokers (Kafka, Pulsar, RabbitMQ) * des mocks de cloud (Google Cloud, LocalStack, Minio) * ... tout ce qui peut tourner dans un container --- ## TestContainers ```java import org.junit.jupiter.api.Test; import org.testcontainers.containers.ElasticsearchContainer; import org.testcontainers.junit.jupiter.*; @Testcontainers @SpringBootTest class ElasticsearchIT { @Container @ServiceConnection static ElasticsearchContainer> elastic = new ElasticsearchContainer<>("elasticsearch:8"); @Autowired // contiendra les infos de connection au container private ElasticsearchConnectionDetails connectionDetails; @Test void myTest() { // ... } } ``` --- ## TestContainers ![](images/spring-boot-test-testcontainers.png) --- # WireMock ([doc](https://wiremock.org/)) Permet de créer des Mock d'API. Utile pour développer quand une API n'existe pas ou est compliquée à appeler dans des tests (ex: l'API de GitHub !). Configuration des requêtes / réponses du mock via des fichiers JSON. Pas de support officiel de Spring Boot, à configurer manuellement. --- ## WireMock stubs `src/test/resources/mappings/bulbizarre.json` ```json { "request": { "method": "GET", "url": "/pokemon-types/1" }, "response": { "status": 200, "jsonBody": { "id": 1, "name": "Bulbizarre" }, "headers": { "Content-Type": "application/json" } } } ``` --- ## WireMock Setup ```xml
org.wiremock
wiremock
3.3.1
test
``` --- ## WireMock Injection dynamique de properties dans l'environnement de test ```java @WireMockTest @SpringBootTest class WireMockIT { @Autowired RestTemplate restTemplate; @BeforeAll static void injectProperties( WireMockRuntimeInfo wmRuntimeInfo, @Autowired ConfigurableEnvironment environment) { TestPropertyValues .of("pokemon-type.service.url="+wmRuntimeInfo.getHttpBaseUrl()) .applyTo(environment); } @Test void callToWireMock( @Value("${pokemon-type.service.url}") String serviceUrl) { assertThat(serviceUrl).isNotNull(); var bulbi = this.restTemplate.getForObject(serviceUrl + "/pokemon-types/1", PokemonType.class); assertThat(bulbi).isNotNull(); assertThat(bulbi.getName()).isEqualTo("Bulbizarre"); } } ``` --- # Cucumber ([doc](https://cucumber.io/docs/cucumber/)) Behaviour Driver Development Language d'écriture de tests fonctionnels : *Gherkin* ```gherkin Feature: Pokemon API Scenario: Retrieving Pokemon Information Given the Pokemon API is available When a user requests information for Pokemon with ID 25 Then the API should respond with status code 200 And the response should contain the following details: | Field | Value | | id | 25 | | name | Pikachu | Scenario: Retrieving Pokemon List Given the Pokemon API is available When a user requests the list of all Pokemon Then the API should respond with status code 200 And the response should contain a list of Pokemon with at least 3 entries ``` --- ## Cucumber Développement d'une _glue_ pour interpréter le _Gherkin_. ```java import io.cucumber.java.en.Given; import io.cucumber.java.en.When; import io.cucumber.java.en.Then; import io.cucumber.java.en.And; import io.cucumber.java.en.But; public class PokemonApiStepDefinitions { @Given("the Pokemon API is available") public void givenThePokemonAPIIsAvailable() { // Code pour configurer l'état initial } @When("a user requests information for Pokemon with ID {int}") public void whenAUserRequestsInformationForPokemonWithID(int pokemonId) { // Code pour effectuer la requête à l'API avec l'ID spécifié } @Then("the API should respond with status code {int}") public void thenTheAPIShouldRespondWithStatusCode(int expectedStatusCode) { // Code pour vérifier le code de statut de la réponse } @And("the response should contain the following details:") public void andTheResponseShouldContainTheFollowingDetails(DataTable dataTable) { // Code pour vérifier les détails de la réponse en fonction des données dans la table } } ``` --- ## Cucumber ```xml
org.junit.platform
junit-platform-suite
test
io.cucumber
cucumber-java
7.15.0
test
io.cucumber
cucumber-spring
7.15.0
test
``` --- ## Cucumber Les fichiers _gherkin_ vont dans `src/test/resources/features`. Intégration avec Spring Boot : ```java @CucumberContextConfiguration // fait le lien avec Cucumber ! @SpringBootTest public class CucumberGlue { @Given("the Pokemon API is available") public void givenThePokemonAPIIsAvailable() { ... } ... } ``` --- ## Configuration JUnit ```java import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") public class CucumberRunnerTest { // cette classe ajoute les tests Cucumber aux tests que JUnit doit exécuter } ``` --- # Fin du cours !