# 🧪 TESTS - BTPXPRESS BACKEND ## 📋 Table des matières - [Vue d'ensemble](#vue-densemble) - [Types de tests](#types-de-tests) - [Configuration](#configuration) - [Tests unitaires](#tests-unitaires) - [Tests d'intégration](#tests-dintégration) - [Couverture de code](#couverture-de-code) - [Bonnes pratiques](#bonnes-pratiques) - [CI/CD](#cicd) --- ## 🎯 Vue d'ensemble ### **Framework de tests** - **JUnit 5** : Framework de tests - **Mockito** : Mocking - **RestAssured** : Tests API REST - **Testcontainers** : Tests avec Docker - **H2** : Base de données en mémoire pour tests ### **Objectifs de couverture** | Type | Objectif | Actuel | |------|----------|--------| | **Ligne** | 80% | - | | **Branche** | 70% | - | | **Méthode** | 85% | - | --- ## 📚 Types de tests ### **1. Tests unitaires** Testent une unité de code isolée (méthode, classe). **Localisation** : `src/test/java/.../service/` **Exemple** : `ChantierServiceTest.java` ### **2. Tests d'intégration** Testent l'intégration entre plusieurs composants. **Localisation** : `src/test/java/.../adapter/http/` **Exemple** : `ChantierResourceTest.java` ### **3. Tests end-to-end** Testent l'application complète avec base de données réelle. **Localisation** : `src/test/java/.../e2e/` --- ## ⚙️ Configuration ### **application-test.properties** ```properties # Base de données H2 en mémoire quarkus.datasource.db-kind=h2 quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.sql-load-script=import-test.sql # Désactiver Keycloak pour les tests quarkus.oidc.enabled=false # Logs quarkus.log.level=INFO quarkus.log.category."dev.lions.btpxpress".level=DEBUG ``` ### **Dépendances Maven** ```xml io.quarkus quarkus-junit5 test io.rest-assured rest-assured test io.quarkus quarkus-junit5-mockito test org.testcontainers postgresql test ``` --- ## 🔬 Tests unitaires ### **Exemple : ChantierServiceTest** ```java @QuarkusTest class ChantierServiceTest { @Inject ChantierService chantierService; @InjectMock ClientService clientService; @Test @DisplayName("Devrait créer un chantier avec succès") void shouldCreateChantier() { // Given ChantierDTO dto = ChantierDTO.builder() .nom("Villa Moderne") .code("CHANT-001") .clientId(UUID.randomUUID()) .build(); Client client = new Client(); client.setId(dto.getClientId()); when(clientService.findById(dto.getClientId())) .thenReturn(Optional.of(client)); // When Chantier result = chantierService.create(dto); // Then assertNotNull(result); assertEquals("Villa Moderne", result.getNom()); assertEquals("CHANT-001", result.getCode()); verify(clientService, times(1)).findById(dto.getClientId()); } @Test @DisplayName("Devrait lever une exception si le client n'existe pas") void shouldThrowExceptionWhenClientNotFound() { // Given ChantierDTO dto = ChantierDTO.builder() .nom("Villa Moderne") .clientId(UUID.randomUUID()) .build(); when(clientService.findById(dto.getClientId())) .thenReturn(Optional.empty()); // When & Then assertThrows(NotFoundException.class, () -> { chantierService.create(dto); }); } @Test @DisplayName("Devrait calculer le montant total correctement") void shouldCalculateTotalAmount() { // Given Chantier chantier = new Chantier(); chantier.setMontantPrevu(new BigDecimal("100000.00")); // When BigDecimal total = chantierService.calculateTotal(chantier); // Then assertEquals(new BigDecimal("100000.00"), total); } } ``` ### **Commandes** ```bash # Exécuter tous les tests unitaires ./mvnw test # Exécuter un test spécifique ./mvnw test -Dtest=ChantierServiceTest # Exécuter une méthode spécifique ./mvnw test -Dtest=ChantierServiceTest#shouldCreateChantier ``` --- ## 🔗 Tests d'intégration ### **Exemple : ChantierResourceTest** ```java @QuarkusTest @TestHTTPEndpoint(ChantierResource.class) class ChantierResourceTest { @Test @DisplayName("GET /chantiers devrait retourner la liste des chantiers") void shouldGetAllChantiers() { given() .when() .get() .then() .statusCode(200) .contentType(ContentType.JSON) .body("size()", greaterThan(0)); } @Test @DisplayName("POST /chantiers devrait créer un chantier") @Transactional void shouldCreateChantier() { // Given ChantierDTO dto = ChantierDTO.builder() .nom("Villa Test") .code("TEST-001") .clientId(createTestClient()) .statut(StatutChantier.PLANIFIE) .build(); // When & Then given() .contentType(ContentType.JSON) .body(dto) .when() .post() .then() .statusCode(201) .body("nom", equalTo("Villa Test")) .body("code", equalTo("TEST-001")); } @Test @DisplayName("GET /chantiers/{id} devrait retourner 404 si non trouvé") void shouldReturn404WhenChantierNotFound() { UUID randomId = UUID.randomUUID(); given() .pathParam("id", randomId) .when() .get("/{id}") .then() .statusCode(404); } @Test @DisplayName("PUT /chantiers/{id} devrait modifier un chantier") @Transactional void shouldUpdateChantier() { // Given UUID chantierId = createTestChantier(); ChantierDTO updateDto = ChantierDTO.builder() .nom("Villa Modifiée") .build(); // When & Then given() .contentType(ContentType.JSON) .pathParam("id", chantierId) .body(updateDto) .when() .put("/{id}") .then() .statusCode(200) .body("nom", equalTo("Villa Modifiée")); } @Test @DisplayName("DELETE /chantiers/{id} devrait supprimer un chantier") @Transactional void shouldDeleteChantier() { // Given UUID chantierId = createTestChantier(); // When & Then given() .pathParam("id", chantierId) .when() .delete("/{id}") .then() .statusCode(204); // Vérifier que le chantier est supprimé given() .pathParam("id", chantierId) .when() .get("/{id}") .then() .statusCode(404); } private UUID createTestClient() { Client client = new Client(); client.setNom("Test Client"); client.setEmail("test@example.com"); client.persist(); return client.getId(); } private UUID createTestChantier() { Chantier chantier = new Chantier(); chantier.setNom("Test Chantier"); chantier.setCode("TEST-" + System.currentTimeMillis()); chantier.setStatut(StatutChantier.PLANIFIE); chantier.persist(); return chantier.getId(); } } ``` ### **Commandes** ```bash # Exécuter tous les tests d'intégration ./mvnw verify # Exécuter un test spécifique ./mvnw verify -Dit.test=ChantierResourceTest ``` --- ## 📊 Couverture de code ### **JaCoCo** Configuration dans `pom.xml` : ```xml org.jacoco jacoco-maven-plugin 0.8.10 prepare-agent report test report ``` ### **Générer le rapport** ```bash # Exécuter les tests avec couverture ./mvnw clean test jacoco:report # Ouvrir le rapport open target/site/jacoco/index.html ``` ### **Rapport de couverture** Le rapport affiche : - Couverture par package - Couverture par classe - Lignes couvertes/non couvertes - Branches couvertes/non couvertes --- ## ✅ Bonnes pratiques ### **1. Nommage des tests** ```java // ❌ Mauvais @Test void test1() { } // ✅ Bon @Test @DisplayName("Devrait créer un chantier avec succès") void shouldCreateChantierSuccessfully() { } ``` ### **2. Pattern AAA (Arrange-Act-Assert)** ```java @Test void shouldCalculateTotal() { // Arrange (Given) Chantier chantier = new Chantier(); chantier.setMontantPrevu(new BigDecimal("100000")); // Act (When) BigDecimal total = service.calculateTotal(chantier); // Assert (Then) assertEquals(new BigDecimal("100000"), total); } ``` ### **3. Tests indépendants** Chaque test doit être indépendant et pouvoir s'exécuter seul. ```java @BeforeEach void setUp() { // Initialiser les données de test } @AfterEach void tearDown() { // Nettoyer les données de test } ``` ### **4. Utiliser des données de test réalistes** ```java // ❌ Mauvais Chantier chantier = new Chantier(); chantier.setNom("test"); // ✅ Bon Chantier chantier = Chantier.builder() .nom("Construction Villa Moderne") .code("CHANT-2025-001") .adresse("123 Rue de la Paix, 75001 Paris") .montantPrevu(new BigDecimal("250000.00")) .build(); ``` --- ## 🚀 CI/CD ### **GitHub Actions** `.github/workflows/tests.yml` : ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Run tests run: ./mvnw clean verify - name: Generate coverage report run: ./mvnw jacoco:report - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 ``` --- **Dernière mise à jour**: 2025-09-30 **Version**: 1.0 **Auteur**: Équipe BTPXpress