9.9 KiB
9.9 KiB
🧪 TESTS - BTPXPRESS BACKEND
📋 Table des matières
- Vue d'ensemble
- Types de tests
- Configuration
- Tests unitaires
- Tests d'intégration
- Couverture de code
- Bonnes pratiques
- CI/CD
🎯 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