Tests

Tests End-to-End pour Spring Boot : Testcontainers, WireMock et Stratégies CI

Guide complet tests E2E Spring Boot 2026 : Testcontainers, WireMock, stratégies CI/CD. Code testé, performances, best practices.

Jean-Michel Helem

Jean-Michel Helem

9 février 2026 · 9 min de lecture

Tests End-to-End pour Spring Boot : Testcontainers, WireMock et Stratégies CI

Les tests end-to-end (E2E) sont le dernier rempart avant la production. Pourtant, dans 70% des projets Spring Boot, les tests E2E sont soit absents, soit si lents qu'ils sont désactivés en CI.

Le problème ? Dépendances externes complexes : bases de données, queues, APIs tierces, storage... Résultat : tests flaky, environnements de test fragiles, bugs en production.

2026 marque un tournant : Testcontainers devient le standard, WireMock automatise les mocks HTTP, et les pipelines CI exécutent des tests E2E en moins de 5 minutes.

Dans cet article, nous construisons une suite de tests E2E complète pour une application Spring Boot moderne, avec :

  • Testcontainers pour PostgreSQL, Redis, Kafka
  • WireMock pour mocker les APIs tierces
  • Stratégies CI optimisées (GitHub Actions)
  • Métriques de qualité (couverture, performances)
  • Nous testons une application e-commerce simplifiée :
  • Stack technique :
  • Spring Boot 3.2
  • PostgreSQL 16
  • Redis 7.2
  • Kafka 3.6
  • WireMock 3.3
  • Avantages de withReuse(true) :
  • ✅ Containers réutilisés entre exécutions de tests
  • Temps de démarrage : 15s → 2s après premier lancement
  • ✅ Nécessite Testcontainers daemon : ~/.testcontainers.properties
  • Temps d'exécution typiques :
  • Configuration parallélisation :
  • 1. Isoler les données entre tests
  • 2. Utiliser des builders pour les fixtures
  • 3. Tester les cas limites
  • 1. Ne pas partager l'état entre tests
  • 2. Ne pas hardcoder les ports
  • 3. Ne pas ignorer les timeouts
  • Les tests E2E avec Testcontainers + WireMock offrent :
  • Environnement reproductible (Docker)
  • Tests réalistes (vraies dépendances)
  • CI rapide (<3 minutes avec optimisations)
  • Confiance avant production
  • Checklist implémentation :
  • [ ] Testcontainers configuré avec reuse=true
  • [ ] WireMock pour toutes les APIs externes
  • [ ] Tests parallèles activés
  • [ ] Couverture > 80%
  • [ ] Pipeline CI < 5 minutes
  • Maillage interne :
  • TDD Assisté par IA : Guide Complet
  • CI/CD avec GitHub Actions : Pipeline Moderne
  • Déployer Spring Boot sur Kubernetes
  • WebFlux et Programmation Réactive

Conclusion

// ❌ MAUVAIS
kafkaLatch.await();  // Bloque indéfiniment// ✅ BON
boolean received = kafkaLatch.await(10, TimeUnit.SECONDS);
// ❌ MAUVAIS
@Value("8080")
private int port;// ✅ BON
@LocalServerPort
// ❌ MAUVAIS
static Order sharedOrder;  // State partagé// ✅ BON

❌ À ÉVITER

@ParameterizedTest
@ValueSource(ints = {-1, 0, 1, 1000, Integer.MAX_VALUE})
void shouldValidateQuantity(int quantity) {
    // Test validation pour toutes les valeurs
public class OrderFixtures {
    public static Order.OrderBuilder defaultOrder() {
        return Order.builder()
                .customerId("customer_test")
                .status(OrderStatus.PENDING)
                .totalAmount(BigDecimal.valueOf(100.0));
    }
@BeforeEach
void cleanupDatabase() {
    orderRepository.deleteAll();
    productRepository.deleteAll();

✅ À FAIRE

Best Practices et Pièges à Éviter

application-test.yml
management:
  metrics:
    export:
      prometheus:
        enabled: true
  endpoints:
    web:
      exposure:
        include: prometheus,health,metricsspring:
  test:
    mockmvc:

Intégration Prometheus

@ExtendWith(TestResultLogger.class)
class OrderE2ETest extends AbstractIntegrationTest {
    // Tests...
}class TestResultLogger implements TestWatcher {
    private static final Map metrics = new ConcurrentHashMap<>();    @Override
    public void testSuccessful(ExtensionContext context) {
        recordMetric(context, "SUCCESS");
    }    @Override
    public void testFailed(ExtensionContext context, Throwable cause) {
        recordMetric(context, "FAILED");
    }    private void recordMetric(ExtensionContext context, String status) {
        String testName = context.getDisplayName();
        long duration = context.getExecutionException()
                .map(e -> System.currentTimeMillis())
                .orElse(0L);        metrics.put(testName, TestMetrics.builder()
                .name(testName)
                .status(status)
                .duration(duration)
                .timestamp(Instant.now())
                .build());
    }    @AfterAll
    static void publishMetrics() {
        // Publier vers système de monitoring
        MetricsPublisher.publish(metrics);
    }

Dashboard de Qualité

Métriques et Reporting

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Execution(ExecutionMode.CONCURRENT)
class ParallelE2ETest extends AbstractIntegrationTest {
    // Tests exécutés en parallèle
    // Attention : isolation des données nécessaire
StratégieTemps totalDétail
--------------------------------
Sans optimisation12 minDémarrage containers à chaque test
Avec reuse4 minContainers réutilisés
Avec reuse + parallel2.5 min4 threads parallèles

Optimisation des Performances



    
        e2e-tests
        
            
                
                
                    org.apache.maven.plugins
                    maven-failsafe-plugin
                    3.2.3
                    
                        
                            /*E2ETest.java
                            /*IntegrationTest.java
                        
                        classes
                        4
                        
                            true
                        
                    
                    
                        
                            
                                integration-test
                                verify
                            
                        
                    
                                
                
                    org.jacoco
                    jacoco-maven-plugin
                    0.8.11
                    
                        
                            
                                prepare-agent
                            
                        
                        
                            report
                            verify
                            
                                report
                            
                        
                        
                            check
                            
                                check
                            
                            
                                
                                    
                                        PACKAGE
                                        
                                            
                                                LINE
                                                COVEREDRATIO
                                                0.80
                                            
                                        
                                    
                                
                            
                        
                    
                
            
        
    

Profil Maven pour Tests E2E

.github/workflows/tests.yml
name: E2E Testson:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]jobs:
  e2e-tests:
    runs-on: ubuntu-latest    services:
      # Docker-in-Docker pour Testcontainers
      docker:
        image: docker:24-dind
        options: --privileged    steps:
      - name: Checkout code
        uses: actions/checkout@v4      - name: Setup Java 21
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'
          cache: 'maven'      - name: Cache Testcontainers images
        uses: actions/cache@v4
        with:
          path: ~/.testcontainers
          key: ${{ runner.os }}-testcontainers-${{ hashFiles('/pom.xml') }}
          restore-keys: |
            ${{ runner.os }}-testcontainers-      - name: Enable Testcontainers reuse
        run: |
          mkdir -p ~/.testcontainers
          echo "testcontainers.reuse.enable=true" > ~/.testcontainers/testcontainers.properties      - name: Run E2E tests
        run: mvn verify -P e2e-tests
        env:
          TESTCONTAINERS_RYUK_DISABLED: false
          DOCKER_HOST: unix:///var/run/docker.sock      - name: Publish test results
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: always()
        with:
          files: |
            /target/surefire-reports/*.xml
            /target/failsafe-reports/*.xml      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: ./target/site/jacoco/jacoco.xml
          flags: e2e-tests      - name: Archive test logs
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: test-logs
          path: |
            /target/surefire-reports
            /logs/*.log

Pipeline GitHub Actions Optimisé

Stratégies CI/CD

@BeforeEach
void setupRealisticPaymentMocks() {
    // Succès (90% des cas)
    stubFor(post(urlEqualTo("/payments"))
            .withRequestBody(matchingJsonPath("$.amount", matching("^[0-9]{1,3}\\.[0-9]{2}$")))
            .willReturn(aResponse()
                    .withStatus(200)
                    .withTransformers("response-template")
                    .withBody("""
                            {
                              "paymentId": "pay_{{randomValue type='UUID'}}",
                              "status": "APPROVED",
                              "transactionDate": "{{now format='yyyy-MM-dd HH:mm:ss'}}",
                              "processingTime": {{randomValue type='NUMBER' lower=100 upper=500}}
                            }
                            """)));    // Échec carte refusée (5%)
    stubFor(post(urlEqualTo("/payments"))
            .withRequestBody(matchingJsonPath("$.customerId", containing("fail")))
            .willReturn(aResponse()
                    .withStatus(402)
                    .withBody("""
                            {
                              "error": "CARD_DECLINED",
                              "message": "Insufficient funds"
                            }
                            """)));    // Timeout (5%)
    stubFor(post(urlEqualTo("/payments"))
            .withRequestBody(matchingJsonPath("$.customerId", containing("slow")))
            .willReturn(aResponse()
                    .withStatus(200)
                    .withFixedDelay(5000)));

Mock de Réponses Réalistes

@Test
@DisplayName("Should retry payment on temporary failures")
void shouldRetryPaymentOnTemporaryFailures() {
    // GIVEN: Scénario WireMock : 2 échecs puis succès
    stubFor(post(urlEqualTo("/payments"))
            .inScenario("Payment Retry")
            .whenScenarioStateIs(STARTED)
            .willReturn(aResponse().withStatus(503))  // Service unavailable
            .willSetStateTo("First Failure"));    stubFor(post(urlEqualTo("/payments"))
            .inScenario("Payment Retry")
            .whenScenarioStateIs("First Failure")
            .willReturn(aResponse().withStatus(503))
            .willSetStateTo("Second Failure"));    stubFor(post(urlEqualTo("/payments"))
            .inScenario("Payment Retry")
            .whenScenarioStateIs("Second Failure")
            .willReturn(aResponse()
                    .withStatus(200)
                    .withBody("{\"paymentId\": \"pay_retry_success\", \"status\": \"APPROVED\"}")));    // WHEN: Créer commande (avec retry configuré dans l'app)
    given()
            .contentType(ContentType.JSON)
            .body("""
                    {
                      "customerId": "customer_retry",
                      "items": [{"productId": "product_123", "quantity": 1, "unitPrice": 49.99}],
                      "paymentMethod": "CREDIT_CARD"
                    }
                    """)
            .when()
            .post("/orders")
            .then()
            .statusCode(201)
            .body("status", equalTo("CONFIRMED"));    // THEN: 3 tentatives effectuées
    verify(exactly(3), postRequestedFor(urlEqualTo("/payments")));

Simulation d'API avec États

WireMock Avancé : Scénarios et Stateful

@Test
@DisplayName("Should prevent overselling with concurrent orders")
void shouldPreventOversellingWithConcurrentOrders() throws InterruptedException {
    // GIVEN: Stock limité (5 unités)
    productRepository.save(Product.builder()
            .productId("limited_product")
            .name("Limited Edition Item")
            .stock(5)
            .price(99.99)
            .build());    stubFor(post(urlEqualTo("/payments"))
            .willReturn(aResponse()
                    .withStatus(200)
                    .withBody("{\"paymentId\": \"pay_concurrent\", \"status\": \"APPROVED\"}")));    // WHEN: 10 commandes concurrentes (chacune pour 1 unité)
    int concurrentOrders = 10;
    CountDownLatch startLatch = new CountDownLatch(1);
    CountDownLatch doneLatch = new CountDownLatch(concurrentOrders);
    AtomicInteger successCount = new AtomicInteger(0);
    AtomicInteger failureCount = new AtomicInteger(0);    for (int i = 0; i < concurrentOrders; i++) {
        new Thread(() -> {
            try {
                startLatch.await();  // Attendre signal de départ                int statusCode = given()
                        .contentType(ContentType.JSON)
                        .body("""
                                {
                                  "customerId": "customer_%s",
                                  "items": [{"productId": "limited_product", "quantity": 1, "unitPrice": 99.99}],
                                  "paymentMethod": "CREDIT_CARD"
                                }
                                """.formatted(UUID.randomUUID()))
                        .when()
                        .post("/orders")
                        .then()
                        .extract()
                        .statusCode();                if (statusCode == 201) {
                    successCount.incrementAndGet();
                } else if (statusCode == 409) {  // OUT_OF_STOCK
                    failureCount.incrementAndGet();
                }
            } catch (Exception e) {
                failureCount.incrementAndGet();
            } finally {
                doneLatch.countDown();
            }
        }).start();
    }    // Démarrer toutes les threads en même temps
    startLatch.countDown();
    doneLatch.await(30, TimeUnit.SECONDS);    // THEN: Seulement 5 commandes acceptées
    assertThat(successCount.get()).isEqualTo(5);
    assertThat(failureCount.get()).isEqualTo(5);    // Vérifier stock final
    Product product = productRepository.findById("limited_product").orElseThrow();
    assertThat(product.getStock()).isZero();

Test 2 : Gestion du Stock avec Transactions

package com.example.ecommerce.orders;import com.example.ecommerce.AbstractIntegrationTest;
import com.github.tomakehurst.wiremock.WireMockServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.test.context.EmbeddedKafka;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderE2ETest extends AbstractIntegrationTest {    @Autowired
    private OrderRepository orderRepository;    private static WireMockServer wireMockServer;
    private static CountDownLatch kafkaLatch;    @BeforeAll
    static void setupWireMock() {
        wireMockServer = new WireMockServer(8090);
        wireMockServer.start();
        configureFor("localhost", 8090);
    }    @AfterAll
    static void teardownWireMock() {
        wireMockServer.stop();
    }    @BeforeEach
    void setup() {
        RestAssured.port = port;
        RestAssured.basePath = "/api";        // Reset WireMock stubs
        wireMockServer.resetAll();        // Reset Kafka latch
        kafkaLatch = new CountDownLatch(1);
    }    @Test
    @Order(1)
    @DisplayName("Should create order with successful payment")
    void shouldCreateOrderWithSuccessfulPayment() {
        // GIVEN: Mock payment API (succès)
        stubFor(post(urlEqualTo("/payments"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("""
                                {
                                  "paymentId": "pay_123456",
                                  "status": "APPROVED",
                                  "transactionDate": "2026-02-10T10:30:00Z"
                                }
                                """)));        // WHEN: Créer commande
        String orderId = given()
                .contentType(ContentType.JSON)
                .body("""
                        {
                          "customerId": "customer_001",
                          "items": [
                            {
                              "productId": "product_123",
                              "quantity": 2,
                              "unitPrice": 49.99
                            },
                            {
                              "productId": "product_456",
                              "quantity": 1,
                              "unitPrice": 29.99
                            }
                          ],
                          "paymentMethod": "CREDIT_CARD",
                          "shippingAddress": {
                            "street": "123 Main St",
                            "city": "Paris",
                            "zipCode": "75001",
                            "country": "FR"
                          }
                        }
                        """)
                .when()
                .post("/orders")
                .then()
                .statusCode(201)
                .body("orderId", notNullValue())
                .body("status", equalTo("CONFIRMED"))
                .body("totalAmount", equalTo(129.97f))
                .extract()
                .path("orderId");        // THEN: Vérifier état base de données
        Order order = orderRepository.findById(orderId).orElseThrow();
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
        assertThat(order.getPaymentId()).isEqualTo("pay_123456");
        assertThat(order.getItems()).hasSize(2);        // Vérifier appel Payment API
        verify(exactly(1), postRequestedFor(urlEqualTo("/payments"))
                .withHeader("Content-Type", equalTo("application/json"))
                .withRequestBody(matchingJsonPath("$.amount", equalTo("129.97")))
                .withRequestBody(matchingJsonPath("$.currency", equalTo("EUR"))));
    }    @Test
    @Order(2)
    @DisplayName("Should reject order when payment fails")
    void shouldRejectOrderWhenPaymentFails() {
        // GIVEN: Mock payment API (échec)
        stubFor(post(urlEqualTo("/payments"))
                .willReturn(aResponse()
                        .withStatus(402)
                        .withHeader("Content-Type", "application/json")
                        .withBody("""
                                {
                                  "error": "INSUFFICIENT_FUNDS",
                                  "message": "Card declined"
                                }
                                """)));        // WHEN: Créer commande
        given()
                .contentType(ContentType.JSON)
                .body("""
                        {
                          "customerId": "customer_002",
                          "items": [
                            {
                              "productId": "product_789",
                              "quantity": 1,
                              "unitPrice": 999.99
                            }
                          ],
                          "paymentMethod": "CREDIT_CARD"
                        }
                        """)
                .when()
                .post("/orders")
                .then()
                .statusCode(402)
                .body("error", equalTo("PAYMENT_FAILED"))
                .body("details", containsString("Card declined"));        // THEN: Aucune commande créée
        assertThat(orderRepository.count()).isZero();
    }    @Test
    @Order(3)
    @DisplayName("Should handle payment API timeout gracefully")
    void shouldHandlePaymentTimeout() {
        // GIVEN: Mock payment API avec délai
        stubFor(post(urlEqualTo("/payments"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withFixedDelay(5000)  // 5 secondes (> timeout app)
                        .withBody("{\"status\": \"APPROVED\"}")));        // WHEN: Créer commande
        given()
                .contentType(ContentType.JSON)
                .body("""
                        {
                          "customerId": "customer_003",
                          "items": [{"productId": "product_123", "quantity": 1, "unitPrice": 49.99}],
                          "paymentMethod": "CREDIT_CARD"
                        }
                        """)
                .when()
                .post("/orders")
                .then()
                .statusCode(504)
                .body("error", equalTo("PAYMENT_TIMEOUT"))
                .body("message", containsString("Payment service timeout"));
    }    @Test
    @Order(4)
    @DisplayName("Should publish OrderCreated event to Kafka")
    void shouldPublishOrderCreatedEvent() throws InterruptedException {
        // GIVEN: Listener Kafka (injecté via @KafkaListener dans classe interne)
        stubFor(post(urlEqualTo("/payments"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withBody("{\"paymentId\": \"pay_789\", \"status\": \"APPROVED\"}")));        // WHEN: Créer commande
        String orderId = given()
                .contentType(ContentType.JSON)
                .body("""
                        {
                          "customerId": "customer_004",
                          "items": [{"productId": "product_123", "quantity": 1, "unitPrice": 49.99}],
                          "paymentMethod": "CREDIT_CARD"
                        }
                        """)
                .when()
                .post("/orders")
                .then()
                .statusCode(201)
                .extract()
                .path("orderId");        // THEN: Vérifier event Kafka reçu
        boolean eventReceived = kafkaLatch.await(10, TimeUnit.SECONDS);
        assertThat(eventReceived).isTrue();
    }    // Listener Kafka pour tests
    @KafkaListener(topics = "order-events", groupId = "test-group")
    public void handleOrderEvent(String message) {
        kafkaLatch.countDown();
    }    @Test
    @Order(5)
    @DisplayName("Should cache order in Redis after creation")
    void shouldCacheOrderInRedis() {
        // GIVEN: Mock payment
        stubFor(post(urlEqualTo("/payments"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withBody("{\"paymentId\": \"pay_999\", \"status\": \"APPROVED\"}")));        // WHEN: Créer commande
        String orderId = given()
                .contentType(ContentType.JSON)
                .body("""
                        {
                          "customerId": "customer_005",
                          "items": [{"productId": "product_123", "quantity": 1, "unitPrice": 49.99}],
                          "paymentMethod": "CREDIT_CARD"
                        }
                        """)
                .when()
                .post("/orders")
                .then()
                .statusCode(201)
                .extract()
                .path("orderId");        // THEN: Premier GET (devrait lire depuis cache)
        given()
                .when()
                .get("/orders/" + orderId)
                .then()
                .statusCode(200)
                .header("X-Cache-Hit", equalTo("true"))
                .body("orderId", equalTo(orderId));        // Mesurer performances cache
        long start = System.currentTimeMillis();
        given()
                .when()
                .get("/orders/" + orderId)
                .then()
                .statusCode(200);
        long duration = System.currentTimeMillis() - start;        // Cache devrait répondre en <10ms
        assertThat(duration).isLessThan(10);
    }

Test 1 : Création de Commande Complète

Tests E2E avec Testcontainers

~/.testcontainers.properties
package com.example.ecommerce;import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public abstract class AbstractIntegrationTest {    @LocalServerPort
    protected int port;    // PostgreSQL container
    @Container
    static PostgreSQLContainer postgres = new PostgreSQLContainer<>(
            DockerImageName.parse("postgres:16-alpine")
    )
            .withDatabaseName("ecommerce_test")
            .withUsername("test")
            .withPassword("test")
            .withReuse(true);  // Réutiliser entre tests (gain de temps)    // Redis container
    @Container
    static GenericContainer redis = new GenericContainer<>(
            DockerImageName.parse("redis:7.2-alpine")
    )
            .withExposedPorts(6379)
            .withReuse(true);    // Kafka container
    @Container
    static KafkaContainer kafka = new KafkaContainer(
            DockerImageName.parse("confluentinc/cp-kafka:7.5.3")
    )
            .withReuse(true);    // Configuration dynamique des propriétés Spring
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // PostgreSQL
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);        // Redis
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", redis::getFirstMappedPort);        // Kafka
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }    @BeforeAll
    static void setup() {
        // Vérifier que les containers sont démarrés
        if (!postgres.isRunning()) {
            throw new RuntimeException("PostgreSQL container is not running");
        }
        if (!redis.isRunning()) {
            throw new RuntimeException("Redis container is not running");
        }
        if (!kafka.isRunning()) {
            throw new RuntimeException("Kafka container is not running");
        }
    }

Configuration de Base Testcontainers


    
    
        org.springframework.boot
        spring-boot-starter-test
        test
        
    
        org.testcontainers
        testcontainers
        1.19.4
        test
        
        org.testcontainers
        postgresql
        1.19.4
        test
        
        org.testcontainers
        kafka
        1.19.4
        test
        
    
        org.wiremock
        wiremock-standalone
        3.3.1
        test
        
    
        io.rest-assured
        rest-assured
        5.4.0
        test
    

Configuration Maven

Setup Testcontainers

┌─────────────────────────────────────────────────────┐
│           Spring Boot E-Commerce API                 │
├─────────────────────────────────────────────────────┤
│                                                      │
│  POST /orders                                        │
│    ├─ Valide commande                                │
│    ├─ Check stock (PostgreSQL)                       │
│    ├─ Appelle Payment API externe (WireMock)         │
│    ├─ Publie OrderCreated event (Kafka)              │
│    └─ Cache résultat (Redis)                         │
│                                                      │
│  GET /orders/{id}                                    │
│    ├─ Lit depuis cache (Redis)                       │
│    └─ Fallback PostgreSQL                            │
│                                                      │
└─────────────────────────────────────────────────────┘
         ↓                ↓              ↓
   PostgreSQL         Redis          Kafka

Architecture de l'Application Test

Les tests E2E ne sont plus un luxe en 2026 : ils sont la garantie de qualité avant chaque déploiement.

Et vous, quelle est votre stratégie de tests ?

Pour aller plus loin

Articles similaires

Tests E2E generes par IA : Playwright et Claude en pratique
IA

Tests E2E generes par IA : Playwright et Claude en pratique

Les tests bout-en-bout (E2E) ont la reputation paradoxale d'etre a la fois indispensables et insupportables. Indispensables : seuls eux verifient que l'application fonctionne reellement comme un utilisateur l'utilise. Insupportables : flakiness chronique, maintenance lourde, lenteur d'execution. Pendant des annees, beaucoup d'equipes les ont reduits au strict minimum pour eviter cette friction. En 2026, l'arrivee des agents IA capables de generer et de maintenir ces tests change l'equation. Comb

Jean-Michel Helem · 11 juin 2026 · 7 min
Coverage 100% avec un agent IA : louable ou contre-productif
IA

Coverage 100% avec un agent IA : louable ou contre-productif

Atteindre 100 % de couverture de code etait autrefois un objectif inaccessible reserve aux projets a tres fort budget qualite. En 2026, avec un agent IA, c'est devenu trivial. Quelques heures suffisent pour qu'un Cline ou un Cursor en mode agent generent les tests manquants pour chaque ligne non couverte. Le rapport passe au vert. Le badge "100 % coverage" apparait fierement sur le README. Mais cette accessibilite cache un piege : l'objectif lui-meme est-il pertinent ? Les retours d'experience p

Jean-Michel Helem · 10 juin 2026 · 7 min
Mutation testing assiste par IA : trouver les vrais bugs
IA

Mutation testing assiste par IA : trouver les vrais bugs

La couverture de code est une metrique seduisante mais largement trompeuse. Une suite de tests qui execute toutes les lignes peut tres bien ne rien verifier. Le mutation testing repond a cette limitation en mesurant la capacite reelle de vos tests a detecter des bugs. La technique existe depuis les annees 1970 mais reste sous-utilisee a cause de son cout calculatoire et de sa complexite. En 2026, l'IA change cette equation. Elle accelere la generation des mutations pertinentes, selectionne intel

Jean-Michel Helem · 9 juin 2026 · 8 min