494 lines
9.9 KiB
Markdown
494 lines
9.9 KiB
Markdown
# 🧪 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
|
|
<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**
|
|
|
|
```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
|
|
<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**
|
|
|
|
```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
|
|
|