Files
btpxpress-backend/TESTING.md
2025-10-01 01:37:34 +00:00

9.9 KiB

🧪 TESTS - BTPXPRESS BACKEND

📋 Table des matières


🎯 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

# 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

<dependencies>
  <!-- JUnit 5 -->
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
  </dependency>
  
  <!-- RestAssured -->
  <dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
  </dependency>
  
  <!-- Mockito -->
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
  </dependency>
  
  <!-- Testcontainers -->
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

🔬 Tests unitaires

Exemple : ChantierServiceTest

@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

# 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

@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

# 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 :

<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.10</version>
  <executions>
    <execution>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>test</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Générer le rapport

# 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

// ❌ Mauvais
@Test
void test1() { }

// ✅ Bon
@Test
@DisplayName("Devrait créer un chantier avec succès")
void shouldCreateChantierSuccessfully() { }

2. Pattern AAA (Arrange-Act-Assert)

@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.

@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

// ❌ 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 :

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