Formation Java Avancé pour QA

Maîtriser Java 8+ pour construire des frameworks de tests robustes et maintenables

Introduction

Cette formation vous guide dans la maîtrise des fonctionnalités Java 8+ spécifiquement appliquées à l'automatisation de tests. Contrairement aux formations Java généralistes, nous nous concentrons uniquement sur ce qui est utile pour construire des frameworks Selenium, RestAssured, Appium robustes.

Prérequis : Java bases (classes, méthodes, héritage). Connaissance de Selenium/WebDriver recommandée.

Objectifs pédagogiques

  • Maîtriser Selenium 4 (syntaxe Duration, waits modernes)
  • Utiliser lambdas et streams pour manipuler les éléments web
  • Configurer Maven/Gradle avec les bonnes versions
  • Appliquer patterns Builder, Strategy pour les tests
  • Paralléliser les tests avec CompletableFuture

Selenium 4 : Syntaxe moderne

Selenium 4 (version actuelle : 4.35.0) marque une rupture majeure. Le passage au standard W3C WebDriver impose de nouvelles pratiques.

Breaking changes Selenium 3 → 4 :
  • driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)
  • driver.findElementById("login")
  • new WebDriverWait(driver, 30)

1. Waits avec Duration (obligatoire)

Selenium 3 (obsolète)

import java.util.concurrent.TimeUnit;

// ❌ NE FONCTIONNE PLUS
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.manage().timeouts().pageLoadTimeout(30, TimeUnit.SECONDS);

WebDriverWait wait = new WebDriverWait(driver, 15);

Selenium 4 (correct)

import java.time.Duration;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;

// ✅ SYNTAXE MODERNE
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("element")));

2. Méthode findElement unique

Selenium 3 (déprécié)

// ❌ Méthodes supprimées
driver.findElementById("username");
driver.findElementByClassName("btn-primary");
driver.findElementByCssSelector(".login-form");
driver.findElementByXPath("//button[@id='submit']");

Selenium 4 (seule syntaxe valide)

import org.openqa.selenium.By;

// ✅ SEULE MÉTHODE DISPONIBLE
driver.findElement(By.id("username"));
driver.findElement(By.className("btn-primary"));
driver.findElement(By.cssSelector(".login-form"));
driver.findElement(By.xpath("//button[@id='submit']"));

3. Waits avancés avec lambdas

Wait custom avec lambda (Selenium 4)

import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

// Wait jusqu'à ce qu'un élément soit cliquable
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// ✅ Lambda pour condition custom
wait.until(driver -> {
    WebElement element = driver.findElement(By.id("submitBtn"));
    return element.isDisplayed() && element.isEnabled();
});

// ✅ Ou avec ExpectedConditions classique
wait.until(ExpectedConditions.elementToBeClickable(By.id("submitBtn")));

4. Pattern Wait robuste pour framework

Classe Screen avec waits centralisés

import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;

public class Screen {
    private final WebDriver driver;
    private static final int DEFAULT_TIMEOUT = 10;
    
    public Screen(WebDriver driver) {
        this.driver = driver;
    }
    
    public void click(By locator) {
        waitForClickable(locator).click();
    }
    
    public void type(By locator, String text) {
        WebElement element = waitForVisible(locator);
        element.clear();
        element.sendKeys(text);
    }
    
    public boolean isVisible(By locator) {
        try {
            waitForVisible(locator);
            return true;
        } catch (TimeoutException e) {
            return false;
        }
    }
    
    private WebElement waitForVisible(By locator) {
        return new WebDriverWait(driver, Duration.ofSeconds(DEFAULT_TIMEOUT))
            .until(ExpectedConditions.visibilityOfElementLocated(locator));
    }
    
    private WebElement waitForClickable(By locator) {
        return new WebDriverWait(driver, Duration.ofSeconds(DEFAULT_TIMEOUT))
            .until(ExpectedConditions.elementToBeClickable(locator));
    }
}

5. Comparaison Selenium 3 vs 4

Fonctionnalité Selenium 3 (obsolète) Selenium 4 (actuel)
Timeouts TimeUnit.SECONDS Duration.ofSeconds()
Find elements findElementById() findElement(By.id())
WebDriverWait new WebDriverWait(driver, 30) new WebDriverWait(driver, Duration.ofSeconds(30))
Standard JSON Wire Protocol W3C WebDriver
Best practice : Toujours centraliser les waits dans une classe Screen/Core pour éviter la duplication de WebDriverWait partout dans vos tests.

Lambdas & Streams pour QA

Les lambdas Java 8 transforment radicalement la façon d'écrire du code de test. Au lieu de classes anonymes verbeuses, on obtient du code concis et expressif.

1. Lambdas dans les waits Selenium

Avant Java 8 (verbeux)

// ❌ Classe anonyme verbeuse
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(new ExpectedCondition<Boolean>() {
    @Override
    public Boolean apply(WebDriver driver) {
        return driver.findElement(By.id("message")).isDisplayed();
    }
});

Avec lambda (concis)

// ✅ Lambda expression
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(driver -> driver.findElement(By.id("message")).isDisplayed());

2. Filtrer des éléments avec Streams

Récupérer uniquement les éléments visibles

import java.util.List;
import java.util.stream.Collectors;

// Récupérer tous les boutons
List<WebElement> allButtons = driver.findElements(By.tagName("button"));

// ✅ Filtrer uniquement ceux qui sont affichés
List<WebElement> visibleButtons = allButtons.stream()
    .filter(WebElement::isDisplayed)
    .collect(Collectors.toList());

System.out.println("Boutons visibles : " + visibleButtons.size());

3. Mapper et transformer des données

Extraire les textes d'une liste

import java.util.List;
import java.util.stream.Collectors;

// Récupérer tous les éléments d'une liste
List<WebElement> items = driver.findElements(By.cssSelector(".product-item"));

// ✅ Extraire uniquement les textes
List<String> productNames = items.stream()
    .map(WebElement::getText)
    .collect(Collectors.toList());

// Afficher
productNames.forEach(System.out::println);

4. Cas pratique : validation de datasets

Vérifier que tous les prix sont valides

import java.util.List;

List<WebElement> priceElements = driver.findElements(By.cssSelector(".price"));

// ✅ Vérifier qu'aucun prix n'est vide ou négatif
boolean allPricesValid = priceElements.stream()
    .map(WebElement::getText)
    .map(text -> text.replace("€", "").trim())
    .map(Double::parseDouble)
    .allMatch(price -> price > 0);

if (allPricesValid) {
    System.out.println("✅ Tous les prix sont valides");
} else {
    System.out.println("❌ Prix invalides détectés");
}

5. Predicates pour assertions custom

Assertions réutilisables avec Predicate

import java.util.function.Predicate;
import org.junit.jupiter.api.Assertions;

public class CustomAssertions {
    
    public static void assertElement(WebElement element, 
                                    Predicate<WebElement> condition, 
                                    String message) {
        Assertions.assertTrue(condition.test(element), message);
    }
}

// ✅ Utilisation dans les tests
WebElement loginBtn = driver.findElement(By.id("loginBtn"));

CustomAssertions.assertElement(
    loginBtn, 
    e -> e.isEnabled() && e.isDisplayed(),
    "Le bouton login doit être visible et cliquable"
);

6. Streams pour traiter des datasets de test

Générer des utilisateurs de test dynamiquement

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class TestDataGenerator {
    
    public static List<User> generateUsers(int count) {
        return IntStream.range(0, count)
            .mapToObj(i -> UserBuilder.aUser()
                .withEmail("user" + i + "@test.com")
                .withPassword("Test@" + i)
                .build())
            .collect(Collectors.toList());
    }
}

// ✅ Générer 10 utilisateurs en une ligne
List<User> testUsers = TestDataGenerator.generateUsers(10);

// Les utiliser dans les tests
testUsers.forEach(user -> {
    loginSteps.seConnecter(user);
    // ... assertions
});
Résumé : Les lambdas et streams rendent le code de test plus lisible, plus maintenable et plus proche du langage métier.

Maven & Gradle pour QA

La gestion des dépendances est cruciale pour maintenir un framework de tests stable. Maven et Gradle permettent d'automatiser le téléchargement des JAR (Selenium, TestNG, RestAssured) et de gérer les versions de manière centralisée.

Versions actuelles (2025) :
  • Selenium : 4.35.0
  • TestNG : 7.x
  • RestAssured : 5.x
  • Appium : 9.x

1. Configuration Maven (pom.xml)

pom.xml complet pour framework Selenium

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    
    <!-- Identifiants du projet -->
    <groupId>com.monentreprise</groupId>
    <artifactId>selenium-framework</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <!-- Propriétés centralisées -->
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        
        <!-- Versions des dépendances -->
        <selenium.version>4.35.0</selenium.version>
        <testng.version>7.10.2</testng.version>
        <restassured.version>5.5.0</restassured.version>
        <allure.version>2.29.1</allure.version>
    </properties>
    
    <dependencies>
        <!-- Selenium WebDriver -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
        </dependency>
        
        <!-- TestNG -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng.version}</version>
            <scope>test</scope>
        </dependency>
        
        <!-- RestAssured pour API -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>${restassured.version}</version>
            <scope>test</scope>
        </dependency>
        
        <!-- Allure Reports -->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-testng</artifactId>
            <version>${allure.version}</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- Surefire pour exécuter les tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.2</version>
                <configuration>
                    <suiteXmlFiles>
                        <suiteXmlFile>testng.xml</suiteXmlFile>
                    </suiteXmlFiles>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2. Configuration Gradle (build.gradle)

build.gradle équivalent

plugins {
    id 'java'
}

group = 'com.monentreprise'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

// Variables de versions
ext {
    seleniumVersion = '4.35.0'
    testngVersion = '7.10.2'
    restAssuredVersion = '5.5.0'
    allureVersion = '2.29.1'
}

dependencies {
    // Selenium WebDriver
    implementation "org.seleniumhq.selenium:selenium-java:${seleniumVersion}"
    
    // TestNG
    testImplementation "org.testng:testng:${testngVersion}"
    
    // RestAssured
    testImplementation "io.rest-assured:rest-assured:${restAssuredVersion}"
    
    // Allure Reports
    testImplementation "io.qameta.allure:allure-testng:${allureVersion}"
}

test {
    useTestNG() {
        suites 'src/test/resources/testng.xml'
    }
}

// Configuration Java
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

3. Profils d'exécution Maven

Exécuter différents types de tests

<!-- Dans pom.xml -->
<profiles>
    <!-- Profil SMOKE -->
    <profile>
        <id>smoke</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <groups>smoke</groups>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
    
    <!-- Profil REGRESSION -->
    <profile>
        <id>regression</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <groups>regression</groups>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Commandes d'exécution

# Exécuter les tests smoke
mvn clean test -Psmoke

# Exécuter les tests de régression
mvn clean test -Pregression

# Exécuter tous les tests
mvn clean test

# Avec parallélisation
mvn clean test -Dparallel=methods -DthreadCount=4

4. Gestion des versions : bonnes pratiques

Erreurs courantes à éviter :
  • ❌ Utiliser <version>LATEST</version> (instable)
  • ❌ Ne pas fixer les versions (conflits de dépendances)
  • ❌ Mélanger Selenium 3 et 4 (incompatibilité)
Best practices :
  • ✅ Toujours fixer les versions exactes
  • ✅ Centraliser dans <properties>
  • ✅ Vérifier la compatibilité Java (min. Java 11 pour Selenium 4)
  • ✅ Utiliser mvn dependency:tree pour détecter les conflits

5. Mise à jour des dépendances

Vérifier les versions disponibles

# Maven : afficher les updates disponibles
mvn versions:display-dependency-updates

# Gradle : afficher les dépendances obsolètes
gradle dependencyUpdates

Résoudre les conflits de versions

# Maven : arbre de dépendances
mvn dependency:tree

# Gradle : voir les dépendances d'un projet
gradle dependencies

# Exclure une dépendance transitive problématique
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.35.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </exclusion>
    </exclusions>
</dependency>

6. Intégration CI/CD

GitHub Actions avec Maven

name: Tests Selenium

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: Cache Maven packages
        uses: actions/cache@v3
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
      
      - name: Run tests
        run: mvn clean test -Psmoke
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: target/surefire-reports/

7. Tableau comparatif Maven vs Gradle

Critère Maven Gradle
Configuration XML (pom.xml) Groovy/Kotlin DSL
Performance Moyen Rapide (build incrémental)
Courbe d'apprentissage Facile Plus complexe
Adoption QA Très répandu Croissant
Flexibilité Limité (convention over config) Très flexible
Recommandation : Pour débuter en QA, commencez avec Maven (plus simple, mieux documenté). Gradle est intéressant pour des projets complexes nécessitant des builds personnalisés.

Patterns avancés pour QA

Les design patterns ne sont pas réservés au développement applicatif. Appliqués aux frameworks de tests, ils apportent robustesse, réutilisabilité et maintenabilité.

1. Builder Pattern : construction de données de test

Le pattern Builder permet de créer des objets complexes de manière fluide et lisible, essentiel pour générer des datasets de test variés sans duplication.

Builder classique pour User

public class User {
    private String email;
    private String password;
    private String role;
    private boolean active;
    
    // Getters/Setters
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    // ... autres getters/setters
}

public class UserBuilder {
    private String email = "user@test.com";
    private String password = "Test@123";
    private String role = "USER";
    private boolean active = true;
    
    public static UserBuilder aUser() {
        return new UserBuilder();
    }
    
    public UserBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
    
    public UserBuilder withPassword(String password) {
        this.password = password;
        return this;
    }
    
    public UserBuilder withRole(String role) {
        this.role = role;
        return this;
    }
    
    public UserBuilder inactive() {
        this.active = false;
        return this;
    }
    
    public User build() {
        User user = new User();
        user.setEmail(email);
        user.setPassword(password);
        user.setRole(role);
        user.setActive(active);
        return user;
    }
}

// ✅ Utilisation fluide dans les tests
User admin = UserBuilder.aUser()
    .withEmail("admin@test.com")
    .withRole("ADMIN")
    .build();

User inactiveUser = UserBuilder.aUser()
    .withEmail("inactive@test.com")
    .inactive()
    .build();

Builder avec génération dynamique

public class UserBuilder {
    private String email = generateUniqueEmail();
    private String password = "Test@123";
    
    private static String generateUniqueEmail() {
        return "user_" + System.currentTimeMillis() + "@test.com";
    }
    
    private static String generateUniquePassword() {
        return "Pass_" + UUID.randomUUID().toString().substring(0, 8);
    }
    
    public UserBuilder withRandomPassword() {
        this.password = generateUniquePassword();
        return this;
    }
    
    // ... reste du builder
}

// ✅ Chaque utilisateur a un email unique
User user1 = UserBuilder.aUser().build();
User user2 = UserBuilder.aUser().build();
// user1.email != user2.email (pas de conflits)

2. Factory Pattern : catalogue de données prédéfinies

Factory pour cas d'usage standards

public class UserFactory {
    
    public static User standardUser() {
        return UserBuilder.aUser().build();
    }
    
    public static User adminUser() {
        return UserBuilder.aUser()
            .withEmail("admin@test.com")
            .withRole("ADMIN")
            .build();
    }
    
    public static User premiumUser() {
        return UserBuilder.aUser()
            .withRole("PREMIUM")
            .build();
    }
    
    public static User inactiveUser() {
        return UserBuilder.aUser()
            .inactive()
            .build();
    }
}

// ✅ Utilisation simplifiée
@Test
public void testAdminAccess() {
    User admin = UserFactory.adminUser();
    loginSteps.seConnecter(admin);
    // ...
}

3. Strategy Pattern : adapter le comportement selon le contexte

Permet de choisir dynamiquement une stratégie de validation (UI vs API, OCR vs DOM, etc.) sans modifier le code des tests.

Interface Strategy

// Interface commune
public interface PaymentStrategy {
    boolean process(Order order, User user);
}

// Implémentation UI
public class UiPaymentStrategy implements PaymentStrategy {
    private final Screen screen;
    
    public UiPaymentStrategy(Screen screen) {
        this.screen = screen;
    }
    
    @Override
    public boolean process(Order order, User user) {
        screen.click(By.id("payByCard"));
        screen.type(By.id("cardNumber"), "4111111111111111");
        screen.click(By.id("submitPayment"));
        return screen.isVisible(By.id("paymentSuccess"));
    }
}

// Implémentation API
public class ApiPaymentStrategy implements PaymentStrategy {
    private final PaymentApiClient apiClient;
    
    public ApiPaymentStrategy(PaymentApiClient client) {
        this.apiClient = client;
    }
    
    @Override
    public boolean process(Order order, User user) {
        PaymentRequest request = new PaymentRequest(
            order.getId(),
            order.getTotal(),
            user.getCardToken()
        );
        PaymentResponse response = apiClient.charge(request);
        return response.isSuccess();
    }
}

Utilisation avec configuration externe

public class PaymentSteps {
    private PaymentStrategy strategy;
    
    public PaymentSteps(String channel, Screen screen, PaymentApiClient api) {
        // ✅ Choix de la stratégie par configuration
        if ("UI".equalsIgnoreCase(channel)) {
            this.strategy = new UiPaymentStrategy(screen);
        } else if ("API".equalsIgnoreCase(channel)) {
            this.strategy = new ApiPaymentStrategy(api);
        } else {
            throw new IllegalArgumentException("Canal inconnu : " + channel);
        }
    }
    
    public boolean effectuerPaiement(Order order, User user) {
        return strategy.process(order, user);
    }
}

// ✅ Dans les tests
String channel = System.getProperty("payment.channel", "UI");
PaymentSteps payment = new PaymentSteps(channel, screen, apiClient);

boolean success = payment.effectuerPaiement(order, user);
assertTrue(success, "Le paiement doit réussir");

4. Observer Pattern : monitoring et reporting

Observer pour screenshots automatiques

import org.testng.ITestListener;
import org.testng.ITestResult;

public class ScreenshotListener implements ITestListener {
    
    @Override
    public void onTestFailure(ITestResult result) {
        // ✅ Screenshot automatique en cas d'échec
        WebDriver driver = getDriverFromTest(result);
        if (driver != null) {
            String screenshotPath = captureScreenshot(driver, result.getName());
            System.out.println("📸 Screenshot : " + screenshotPath);
            
            // Attacher à Allure Report
            Allure.addAttachment(
                "Échec: " + result.getName(),
                new ByteArrayInputStream(((TakesScreenshot) driver)
                    .getScreenshotAs(OutputType.BYTES))
            );
        }
    }
    
    private String captureScreenshot(WebDriver driver, String testName) {
        File screenshot = ((TakesScreenshot) driver)
            .getScreenshotAs(OutputType.FILE);
        String path = "screenshots/" + testName + "_" + 
                     System.currentTimeMillis() + ".png";
        try {
            Files.copy(screenshot.toPath(), Paths.get(path));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return path;
    }
    
    private WebDriver getDriverFromTest(ITestResult result) {
        Object testInstance = result.getInstance();
        try {
            Field driverField = testInstance.getClass()
                .getDeclaredField("driver");
            driverField.setAccessible(true);
            return (WebDriver) driverField.get(testInstance);
        } catch (Exception e) {
            return null;
        }
    }
}

5. Singleton Pattern : WebDriver instance unique

Attention : Le Singleton est souvent mal utilisé en QA. Il convient uniquement pour des ressources partagées (configuration), jamais pour WebDriver dans des tests parallèles.

Singleton pour Configuration (bon usage)

public class TestConfig {
    private static TestConfig instance;
    private Properties properties;
    
    private TestConfig() {
        properties = new Properties();
        try (InputStream input = getClass()
                .getResourceAsStream("/config.properties")) {
            properties.load(input);
        } catch (IOException e) {
            throw new RuntimeException("Impossible de charger la config", e);
        }
    }
    
    public static TestConfig getInstance() {
        if (instance == null) {
            synchronized (TestConfig.class) {
                if (instance == null) {
                    instance = new TestConfig();
                }
            }
        }
        return instance;
    }
    
    public String getBaseUrl() {
        return properties.getProperty("base.url");
    }
    
    public int getTimeout() {
        return Integer.parseInt(properties.getProperty("timeout", "10"));
    }
}

// ✅ Utilisation
String baseUrl = TestConfig.getInstance().getBaseUrl();

ThreadLocal pour WebDriver en parallèle (bonne pratique)

public class DriverManager {
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
    
    public static void setDriver(WebDriver driverInstance) {
        driver.set(driverInstance);
    }
    
    public static WebDriver getDriver() {
        return driver.get();
    }
    
    public static void quitDriver() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove();
        }
    }
}

// ✅ Dans les tests (thread-safe)
@BeforeMethod
public void setup() {
    WebDriver driver = new ChromeDriver();
    DriverManager.setDriver(driver);
}

@Test
public void test1() {
    WebDriver driver = DriverManager.getDriver();
    driver.get("https://example.com");
}

@AfterMethod
public void teardown() {
    DriverManager.quitDriver();
}

6. Tableau récapitulatif des patterns

Pattern Usage QA Avantages Pièges à éviter
Builder Création de données de test Fluide, flexible, évite duplication Ne pas oublier les valeurs par défaut
Factory Catalogue de cas standards Réutilisabilité, cohérence Ne pas multiplier les factories inutiles
Strategy Multi-canaux (UI/API/OCR) Adaptabilité, testabilité Bien définir l'interface commune
Observer Screenshots, logs, métriques Observabilité automatique Attention aux performances
Singleton Configuration globale Instance unique, facile d'accès ❌ Jamais pour WebDriver en parallèle
ThreadLocal WebDriver en parallèle Thread-safe, isolation Toujours cleanup (memory leaks)
Règle d'or : Les patterns doivent simplifier votre code, pas le compliquer. Si un pattern ajoute de la complexité sans bénéfice clair, c'est qu'il est mal appliqué.

Multithreading & Parallélisation pour QA

L'exécution parallèle des tests est essentielle pour réduire les temps de build. Cependant, elle introduit des défis : race conditions, partage d'état, gestion des ressources. Cette section couvre tous les aspects du multithreading appliqué à la QA.

Contexte : Une suite de 500 tests prend 2h en séquentiel. Avec 10 threads parallèles, on peut réduire à 15-20 minutes. Mais mal configuré, c'est le chaos : deadlocks, tests qui s'entremêlent, résultats incohérents.

1. Concepts fondamentaux du multithreading

Thread-safety : le problème central

Problème : état partagé entre threads

// ❌ MAUVAIS : variable partagée entre tests
public class LoginTest {
    private static WebDriver driver; // ⚠️ Shared state!
    
    @BeforeMethod
    public void setup() {
        driver = new ChromeDriver(); // Race condition!
    }
    
    @Test
    public void testLogin1() {
        driver.get("https://example.com");
        // Thread A démarre
    }
    
    @Test
    public void testLogin2() {
        driver.get("https://example.com");
        // Thread B écrase le driver de A!
    }
}

Solution : ThreadLocal pour isolation

// ✅ BON : isolation par thread
public class DriverManager {
    private static ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();
    
    public static void setDriver(WebDriver driver) {
        driverThread.set(driver);
    }
    
    public static WebDriver getDriver() {
        return driverThread.get();
    }
    
    public static void quitDriver() {
        WebDriver driver = driverThread.get();
        if (driver != null) {
            driver.quit();
            driverThread.remove(); // ⚠️ Important : éviter memory leak
        }
    }
}

public class LoginTest {
    
    @BeforeMethod
    public void setup() {
        WebDriver driver = new ChromeDriver();
        DriverManager.setDriver(driver); // Chaque thread a son driver
    }
    
    @Test
    public void testLogin1() {
        WebDriver driver = DriverManager.getDriver();
        driver.get("https://example.com");
        // Totalement isolé des autres threads
    }
    
    @AfterMethod
    public void teardown() {
        DriverManager.quitDriver();
    }
}

2. Parallélisation avec TestNG

Configuration testng.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Suite Parallèle" parallel="methods" thread-count="5">
    
    <!-- Parallélisation au niveau méthodes -->
    <test name="Tests Login">
        <classes>
            <class name="com.test.LoginTest"/>
            <class name="com.test.SearchTest"/>
        </classes>
    </test>
    
</suite>

<!-- Autres modes de parallélisation -->

<!-- parallel="tests" : chaque <test> dans un thread -->
<suite name="Suite" parallel="tests" thread-count="3">
    <test name="Test1">...</test>
    <test name="Test2">...</test>
</suite>

<!-- parallel="classes" : chaque classe dans un thread -->
<suite name="Suite" parallel="classes" thread-count="4">
    <test name="Tous les tests">
        <classes>
            <class name="LoginTest"/>
            <class name="CheckoutTest"/>
        </classes>
    </test>
</suite>

3. CompletableFuture : parallélisation programmatique

Exécuter des validations en parallèle

import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.stream.Collectors;

public class ParallelValidation {
    
    public static void validateMultipleOrders(List<Order> orders) {
        
        // ✅ Créer une CompletableFuture par commande
        List<CompletableFuture<Boolean>> futures = orders.stream()
            .map(order -> CompletableFuture.supplyAsync(() -> {
                // Chaque validation dans son propre thread
                return validateOrder(order);
            }))
            .collect(Collectors.toList());
        
        // Attendre que toutes les validations soient terminées
        CompletableFuture<Void> allDone = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );
        
        allDone.join(); // Bloque jusqu'à la fin
        
        // Récupérer les résultats
        List<Boolean> results = futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
        
        // Vérifier que toutes ont réussi
        boolean allValid = results.stream().allMatch(result -> result);
        System.out.println("Toutes valides : " + allValid);
    }
    
    private static boolean validateOrder(Order order) {
        // Simulation validation (appel API, vérification DB...)
        try {
            Thread.sleep(1000); // Simule latence
            return order.getTotal() > 0;
        } catch (InterruptedException e) {
            return false;
        }
    }
}

// ✅ Utilisation dans un test
@Test
public void testMassiveOrders() {
    List<Order> orders = OrderFactory.generate(100);
    
    long start = System.currentTimeMillis();
    ParallelValidation.validateMultipleOrders(orders);
    long duration = System.currentTimeMillis() - start;
    
    System.out.println("Validé 100 commandes en " + duration + "ms");
    // Séquentiel : 100s, Parallèle : ~10s
}

4. ExecutorService : contrôle fin des threads

Pool de threads configurable

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

public class ThreadPoolManager {
    
    public static void executeTestsInParallel(List<Runnable> tests, 
                                             int threadCount) {
        
        // ✅ Créer un pool de threads fixe
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        
        List<Future<?>> futures = new ArrayList<>();
        
        // Soumettre chaque test
        for (Runnable test : tests) {
            Future<?> future = executor.submit(test);
            futures.add(future);
        }
        
        // Attendre la fin de tous les tests
        for (Future<?> future : futures) {
            try {
                future.get(); // Bloque jusqu'à la fin du test
            } catch (InterruptedException | ExecutionException e) {
                System.err.println("Test échoué : " + e.getMessage());
            }
        }
        
        // ⚠️ IMPORTANT : shutdown du pool
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

// ✅ Exemple d'utilisation
@Test
public void testParallelExecution() {
    List<Runnable> tests = List.of(
        () -> testLogin(),
        () -> testSearch(),
        () -> testCheckout()
    );
    
    ThreadPoolManager.executeTestsInParallel(tests, 3);
}

private void testLogin() {
    WebDriver driver = new ChromeDriver();
    try {
        driver.get("https://example.com/login");
        // ... test logic
    } finally {
        driver.quit();
    }
}

5. Synchronisation : locks et sémaphores

Problème : ressource partagée limitée

import java.util.concurrent.Semaphore;

// Scénario : seulement 3 licences Chrome disponibles
public class LicenseManager {
    private static final Semaphore licenses = new Semaphore(3);
    
    public static WebDriver acquireDriver() throws InterruptedException {
        // ✅ Attend qu'une licence soit disponible
        licenses.acquire();
        System.out.println(Thread.currentThread().getName() + 
                         " a obtenu une licence");
        return new ChromeDriver();
    }
    
    public static void releaseDriver(WebDriver driver) {
        if (driver != null) {
            driver.quit();
            licenses.release();
            System.out.println(Thread.currentThread().getName() + 
                             " a libéré une licence");
        }
    }
}

// ✅ Utilisation dans les tests
@BeforeMethod
public void setup() throws InterruptedException {
    WebDriver driver = LicenseManager.acquireDriver();
    DriverManager.setDriver(driver);
}

@AfterMethod
public void teardown() {
    LicenseManager.releaseDriver(DriverManager.getDriver());
}

6. Race conditions : détection et correction

Problème classique : compteur partagé

// ❌ MAUVAIS : race condition
public class TestCounter {
    private static int counter = 0; // Non thread-safe!
    
    @Test
    public void test() {
        counter++; // ⚠️ Plusieurs threads écrivent en même temps
        System.out.println("Test #" + counter);
    }
}

// ✅ SOLUTION 1 : AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;

public class TestCounter {
    private static AtomicInteger counter = new AtomicInteger(0);
    
    @Test
    public void test() {
        int testNumber = counter.incrementAndGet(); // Thread-safe
        System.out.println("Test #" + testNumber);
    }
}

// ✅ SOLUTION 2 : synchronized
public class TestCounter {
    private static int counter = 0;
    
    @Test
    public synchronized void test() {
        counter++; // Un seul thread à la fois
        System.out.println("Test #" + counter);
    }
}

7. Parallélisation avec Selenium Grid

Configuration RemoteWebDriver

import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.net.URL;

public class GridDriverFactory {
    
    public static WebDriver createDriver(String browser) {
        try {
            URL gridUrl = new URL("http://localhost:4444/wd/hub");
            
            DesiredCapabilities caps = new DesiredCapabilities();
            caps.setBrowserName(browser);
            
            return new RemoteWebDriver(gridUrl, caps);
        } catch (Exception e) {
            throw new RuntimeException("Impossible de créer le driver", e);
        }
    }
}

// ✅ Parallélisation multi-browsers
@Test(dataProvider = "browsers")
public void testCrossBrowser(String browser) {
    WebDriver driver = GridDriverFactory.createDriver(browser);
    try {
        driver.get("https://example.com");
        // ... test logic
    } finally {
        driver.quit();
    }
}

@DataProvider(name = "browsers", parallel = true)
public Object[][] browsers() {
    return new Object[][] {
        {"chrome"},
        {"firefox"},
        {"edge"}
    };
}

8. Gestion des screenshots en parallèle

Éviter les collisions de fichiers

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ThreadSafeScreenshot {
    
    public static String capture(WebDriver driver, String testName) {
        // ✅ Nom unique par thread et timestamp
        String threadName = Thread.currentThread().getName()
            .replaceAll("[^a-zA-Z0-9]", "_");
        
        String timestamp = LocalDateTime.now()
            .format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS"));
        
        String filename = String.format("%s_%s_%s.png", 
            testName, threadName, timestamp);
        
        Path screenshotPath = Path.of("screenshots", filename);
        
        try {
            Files.createDirectories(screenshotPath.getParent());
            
            File screenshot = ((TakesScreenshot) driver)
                .getScreenshotAs(OutputType.FILE);
            
            Files.copy(screenshot.toPath(), screenshotPath);
            
            return screenshotPath.toString();
        } catch (IOException e) {
            System.err.println("Erreur screenshot : " + e.getMessage());
            return null;
        }
    }
}

// ✅ Utilisation
@Test
public void test() {
    WebDriver driver = DriverManager.getDriver();
    driver.get("https://example.com");
    
    String path = ThreadSafeScreenshot.capture(driver, "testLogin");
    System.out.println("Screenshot : " + path);
    // Résultat : screenshots/testLogin_pool-1-thread-3_20250115_143052_789.png
}

9. Debugging du multithreading

Logger thread-aware

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ThreadAwareLogger {
    private static final Logger log = LoggerFactory.getLogger(ThreadAwareLogger.class);
    
    public static void info(String message) {
        String threadInfo = String.format("[Thread-%s] %s", 
            Thread.currentThread().getName(), 
            message);
        log.info(threadInfo);
    }
    
    public static void error(String message, Throwable e) {
        String threadInfo = String.format("[Thread-%s] %s", 
            Thread.currentThread().getName(), 
            message);
        log.error(threadInfo, e);
    }
}

// ✅ Utilisation
@Test
public void test() {
    ThreadAwareLogger.info("Début du test login");
    // Output: [Thread-pool-1-thread-3] Début du test login
    
    WebDriver driver = DriverManager.getDriver();
    ThreadAwareLogger.info("Driver créé : " + driver.getClass().getSimpleName());
    // Output: [Thread-pool-1-thread-3] Driver créé : ChromeDriver
}

10. Bonnes pratiques multithreading

Problème Symptôme Solution
État partagé Tests s'entremêlent, résultats aléatoires ThreadLocal pour isolation complète
Memory leaks OutOfMemoryError après plusieurs runs Toujours appeler threadLocal.remove()
Deadlocks Tests bloqués indéfiniment Éviter locks multiples, timeout sur wait()
Fichiers collisions Screenshots écrasés Nom avec thread ID + timestamp
Ressources limitées Too many connections Semaphore pour limiter concurrence

11. Checklist parallélisation

Avant de paralléliser, vérifiez :
  • ✅ Aucune variable static/shared entre tests
  • ✅ WebDriver dans ThreadLocal
  • ✅ Données de test uniques (UUID, timestamp)
  • ✅ Screenshots avec noms thread-safe
  • ✅ Logs identifient le thread
  • ✅ Cleanup systématique (threadLocal.remove())
  • ✅ Tests indépendants (pas de dépendances inter-tests)
  • ✅ Thread-count adapté aux ressources (CPU, RAM, licences)

12. Métriques de performance

Mesurer le gain de parallélisation

public class ParallelMetrics {
    
    @Test
    public void compareSequentialVsParallel() {
        List<Runnable> tests = generateTests(50);
        
        // Séquentiel
        long start = System.currentTimeMillis();
        tests.forEach(Runnable::run);
        long sequential = System.currentTimeMillis() - start;
        
        // Parallèle (10 threads)
        start = System.currentTimeMillis();
        ThreadPoolManager.executeTestsInParallel(tests, 10);
        long parallel = System.currentTimeMillis() - start;
        
        // Calcul du speedup
        double speedup = (double) sequential / parallel;
        
        System.out.println("Séquentiel : " + sequential + "ms");
        System.out.println("Parallèle  : " + parallel + "ms");
        System.out.println("Speedup    : " + String.format("%.2fx", speedup));
        
        // Exemple résultat :
        // Séquentiel : 50000ms (50s)
        // Parallèle  : 6000ms (6s)
        // Speedup    : 8.33x
    }
}
Limites du parallélisme :
  • Au-delà de 20 threads, les gains diminuent (overhead)
  • La loi d'Amdahl s'applique : parties séquentielles limitent le speedup
  • Coût mémoire : chaque thread = 1 browser = ~500MB RAM
  • Flakiness accru si tests mal isolés

13. Cas pratique complet : framework parallèle

Architecture complète thread-safe

// 1. DriverManager avec ThreadLocal
public class DriverManager {
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
    
    public static void initDriver() {
        driver.set(new ChromeDriver());
    }
    
    public static WebDriver getDriver() {
        return driver.get();
    }
    
    public static void quitDriver() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove();
        }
    }
}

// 2. TestBase avec gestion thread-safe
public class TestBase {
    
    @BeforeMethod
    public void setup() {
        DriverManager.initDriver();
        ThreadAwareLogger.info("Driver initialisé");
    }
    
    @AfterMethod
    public void teardown(ITestResult result) {
        if (!result.isSuccess()) {
            String screenshot = ThreadSafeScreenshot.capture(
                DriverManager.getDriver(), 
                result.getName()
            );
            ThreadAwareLogger.info("Screenshot : " + screenshot);
        }
        DriverManager.quitDriver();
    }
}

// 3. Tests parallèles
public class LoginTests extends TestBase {
    
    @Test(threadPoolSize = 5, invocationCount = 10)
    public void testLogin() {
        WebDriver driver = DriverManager.getDriver();
        
        // Données uniques par thread
        User user = UserBuilder.aUser().build();
        
        driver.get("https://example.com/login");
        driver.findElement(By.id("email")).sendKeys(user.getEmail());
        driver.findElement(By.id("password")).sendKeys(user.getPassword());
        driver.findElement(By.id("submit")).click();
        
        assertTrue(driver.getCurrentUrl().contains("/dashboard"));
    }
}

// 4. Configuration testng.xml
<suite name="Suite Parallèle" parallel="methods" thread-count="10">
    <test name="Tests Login">
        <classes>
            <class name="LoginTests"/>
        </classes>
    </test>
</suite>
Résultat : Framework 100% thread-safe, capable d'exécuter des centaines de tests en parallèle sans collision ni memory leak. Temps de build divisé par 10.

🛠️ Atelier pratique : Build d'un framework complet

Cet atelier vous guide dans la construction d'un framework Selenium production-ready de A à Z. Contrairement à un simple exemple théorique, nous allons créer une architecture industrielle complète inspirée de frameworks réels utilisés en entreprise.

« Un bon framework de tests n'est pas celui qui fonctionne aujourd'hui, mais celui qui continuera de fonctionner dans 2 ans avec 500 tests en parallèle. »

Objectifs de l'atelier

  • Construire un framework e-commerce avec architecture en couches
  • Appliquer tous les concepts Java 8+ vus dans la formation
  • Obtenir un projet prêt pour la production (parallélisation, reports, CI/CD)
  • Comprendre chaque décision technique et ses alternatives

Architecture cible

Structure finale du framework

selenium-framework/
├── src/
│   ├── main/java/com/ecommerce/
│   │   ├── config/           # Configuration centralisée
│   │   ├── driver/           # Gestion WebDriver (Factory, Manager)
│   │   ├── pages/            # Page Objects
│   │   ├── keywords/         # WebUI (méthodes réutilisables)
│   │   ├── utils/            # Helpers (Logger, Screenshots, etc.)
│   │   └── data/             # Builders & Factories
│   └── test/java/com/ecommerce/
│       ├── tests/            # Scénarios de test
│       ├── listeners/        # TestNG Listeners
│       └── resources/
│           ├── config.properties
│           └── testng.xml
├── pom.xml
└── README.md

Technologies utilisées

Technologie Version Usage
Java 17 Langage de base
Selenium WebDriver 4.35.0 Automatisation navigateur
TestNG 7.10.2 Framework de test
Maven 3.9+ Build & dépendances
Allure 2.29.1 Reporting
Log4j2 2.23.1 Logging
JavaFaker 1.0.2 Génération données
Durée estimée : 2-3h pour suivre l'atelier complet. Prenez le temps de comprendre chaque section avant de passer à la suivante.

Partie 1 : Setup Maven & Structure

La première étape consiste à créer un projet Maven structuré correctement. Maven va gérer automatiquement toutes nos dépendances (Selenium, TestNG, etc.) et nous permettre d'exécuter les tests via ligne de commande ou CI/CD.

1.1 Création du projet Maven

Commande de création (Terminal/CMD)

mvn archetype:generate \
  -DgroupId=com.ecommerce \
  -DartifactId=selenium-framework \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

Cette commande génère la structure de base Maven. Le groupId représente l'organisation (reverse domain), l'artifactId est le nom du projet.

Alternative : Vous pouvez aussi créer le projet directement dans votre IDE (IntelliJ IDEA : File → New → Project → Maven, Eclipse : File → New → Maven Project).

1.2 Configuration pom.xml complet

Le fichier pom.xml est le cœur du projet Maven. Il définit toutes les dépendances (librairies externes) et les plugins nécessaires. Voici la configuration complète avec les versions 2025 vérifiées.

pom.xml production-ready

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.ecommerce</groupId>
    <artifactId>selenium-framework</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>E-commerce Test Framework</name>
    
    <!-- ==================== PROPRIÉTÉS ==================== -->
    <properties>
        <!-- Versions Java -->
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        
        <!-- Versions des librairies (centralisées pour faciliter les updates) -->
        <selenium.version>4.35.0</selenium.version>
        <testng.version>7.10.2</testng.version>
        <allure.version>2.29.1</allure.version>
        <log4j.version>2.23.1</log4j.version>
        <javafaker.version>1.0.2</javafaker.version>
        <commons-io.version>2.16.1</commons-io.version>
    </properties>
    
    <!-- ==================== DÉPENDANCES ==================== -->
    <dependencies>
        
        <!-- Selenium WebDriver -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
        </dependency>
        
        <!-- TestNG -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng.version}</version>
        </dependency>
        
        <!-- Allure Reports -->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-testng</artifactId>
            <version>${allure.version}</version>
        </dependency>
        
        <!-- Log4j2 pour logging -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        
        <!-- JavaFaker pour générer des données de test -->
        <dependency>
            <groupId>com.github.javafaker</groupId>
            <artifactId>javafaker</artifactId>
            <version>${javafaker.version}</version>
        </dependency>
        
        <!-- Commons IO pour gestion fichiers -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons-io.version}</version>
        </dependency>
        
    </dependencies>
    
    <!-- ==================== BUILD PLUGINS ==================== -->
    <build>
        <plugins>
            
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            
            <!-- Surefire Plugin pour exécuter les tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.2</version>
                <configuration>
                    <suiteXmlFiles>
                        <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
                    </suiteXmlFiles>
                    <!-- Permet d'afficher les logs dans la console -->
                    <useSystemClassLoader>false</useSystemClassLoader>
                </configuration>
            </plugin>
            
        </plugins>
    </build>
    
</project>

Explication détaillée des choix :

  • Java 17 : Version LTS (Long Term Support) recommandée en 2025. Compatible avec Selenium 4 et offre les performances optimales.
  • Propriétés centralisées : Les versions sont définies dans <properties> pour faciliter les mises à jour. Changement en un seul endroit au lieu de modifier chaque dépendance.
  • Selenium 4.35.0 : Dernière version stable avec support W3C WebDriver complet.
  • TestNG vs JUnit : TestNG est préféré en QA pour sa gestion native de la parallélisation et des DataProviders.
  • Allure : Système de reporting moderne avec capture d'écran, vidéos, logs intégrés.
  • Log4j2 : Framework de logging performant avec configuration flexible (niveaux, appenders).
  • JavaFaker : Génère des données réalistes (emails, noms, adresses) pour éviter les datasets statiques.
Piège classique : Utiliser <version>LATEST</version> ou ne pas fixer les versions. Cela peut casser vos tests lors d'une mise à jour automatique. Toujours utiliser des versions explicites.

1.3 Structure des dossiers

Une fois le projet créé, nous devons organiser les dossiers selon l'architecture Clean QA. Chaque package a une responsabilité unique.

Commandes pour créer la structure (Linux/Mac)

# Naviguer dans le projet
cd selenium-framework

# Créer l'arborescence src/main/java
mkdir -p src/main/java/com/ecommerce/{config,driver,pages,keywords,utils,data}

# Créer l'arborescence src/test/java
mkdir -p src/test/java/com/ecommerce/{tests,listeners}

# Créer le dossier resources
mkdir -p src/test/resources

Pour Windows (PowerShell)

# Créer src/main/java
New-Item -ItemType Directory -Path "src\main\java\com\ecommerce\config" -Force
New-Item -ItemType Directory -Path "src\main\java\com\ecommerce\driver" -Force
New-Item -ItemType Directory -Path "src\main\java\com\ecommerce\pages" -Force
New-Item -ItemType Directory -Path "src\main\java\com\ecommerce\keywords" -Force
New-Item -ItemType Directory -Path "src\main\java\com\ecommerce\utils" -Force
New-Item -ItemType Directory -Path "src\main\java\com\ecommerce\data" -Force

# Créer src/test/java
New-Item -ItemType Directory -Path "src\test\java\com\ecommerce\tests" -Force
New-Item -ItemType Directory -Path "src\test\java\com\ecommerce\listeners" -Force

# Créer resources
New-Item -ItemType Directory -Path "src\test\resources" -Force

Rôle de chaque package :

Package Contenu Exemples de classes
config Configuration centralisée ConfigManager, Constants
driver Gestion WebDriver DriverManager, BrowserFactory
pages Page Objects (locators) LoginPage, ProductPage, CartPage
keywords Actions réutilisables WebUI (classe centrale)
utils Helpers transversaux LogUtils, ScreenshotUtils, DateUtils
data Builders & Factories UserBuilder, ProductFactory
tests Scénarios de test LoginTest, CheckoutTest
listeners TestNG Listeners TestListener (screenshots auto)
Validation : Après cette étape, votre projet doit compiler avec mvn clean compile. Maven va télécharger toutes les dépendances dans ~/.m2/repository/.

Partie 2 : Core Layer - Fondations du framework

Le Core Layer est la fondation technique du framework. Il contient toutes les classes responsables de la gestion du WebDriver, de la configuration et des mécanismes de base. C'est la couche la plus critique : si elle est mal conçue, tout le framework sera instable.

2.1 DriverManager : Gestion thread-safe du WebDriver

Le DriverManager est responsable de créer, stocker et nettoyer les instances WebDriver. En utilisant ThreadLocal, nous garantissons que chaque thread de test (en cas de parallélisation) a son propre driver isolé.

Pourquoi ThreadLocal ? Imaginez 10 tests qui tournent en parallèle. Sans ThreadLocal, ils partageraient tous le même WebDriver, créant des collisions (Thread A clique sur un bouton pendant que Thread B charge une autre page).

DriverManager.java - Classe complète

package com.ecommerce.driver;

import org.openqa.selenium.WebDriver;

/**
 * Gestionnaire centralisé des instances WebDriver.
 * Utilise ThreadLocal pour garantir l'isolation en mode parallèle.
 */
public class DriverManager {
    
    // ThreadLocal stocke un WebDriver différent par thread
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
    
    /**
     * Définit le WebDriver pour le thread courant
     * @param driverInstance instance WebDriver à stocker
     */
    public static void setDriver(WebDriver driverInstance) {
        driver.set(driverInstance);
    }
    
    /**
     * Récupère le WebDriver du thread courant
     * @return WebDriver instance ou null si non initialisé
     */
    public static WebDriver getDriver() {
        return driver.get();
    }
    
    /**
     * Ferme et nettoie le WebDriver du thread courant.
     * IMPORTANT : Appelle remove() pour éviter les memory leaks.
     */
    public static void quitDriver() {
        WebDriver currentDriver = driver.get();
        
        if (currentDriver != null) {
            try {
                currentDriver.quit();
            } catch (Exception e) {
                System.err.println("Erreur lors de la fermeture du driver : " + e.getMessage());
            } finally {
                // ⚠️ CRITIQUE : Toujours remove() pour libérer la mémoire
                driver.remove();
            }
        }
    }
    
    /**
     * Vérifie si un driver est actif pour le thread courant
     * @return true si un driver existe
     */
    public static boolean hasDriver() {
        return driver.get() != null;
    }
}

Points clés de l'implémentation :

  • ThreadLocal<WebDriver> : Chaque thread a sa propre "copie" du driver. Techniquement, ThreadLocal crée une Map interne (Thread → WebDriver).
  • driver.remove() : Sans cet appel, ThreadLocal garde une référence au WebDriver même après quit(), causant des fuites mémoire (OutOfMemoryError après 50+ tests).
  • try-finally : Le remove() est dans finally pour garantir l'exécution même si quit() lève une exception.
  • hasDriver() : Méthode utilitaire pour vérifier l'état avant d'agir.
Erreur fatale courante : Utiliser private static WebDriver driver (sans ThreadLocal). En parallèle, cela crée un chaos complet : les threads s'entremêlent, les tests échouent aléatoirement (flaky tests à 90%).

2.2 BrowserFactory : Création des drivers par navigateur

La BrowserFactory applique le design pattern Factory pour créer les WebDriver selon le navigateur demandé. Elle centralise toutes les configurations spécifiques (options Chrome, Firefox, etc.) et facilite l'ajout de nouveaux navigateurs.

BrowserFactory.java - Classe complète

package com.ecommerce.driver;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;

import java.time.Duration;

/**
 * Factory pour créer des instances WebDriver selon le navigateur.
 * Centralise toutes les configurations (options, capabilities).
 */
public class BrowserFactory {
    
    /**
     * Crée un WebDriver pour le navigateur spécifié
     * @param browserName nom du navigateur (chrome, firefox, edge)
     * @return WebDriver configuré
     */
    public static WebDriver createDriver(String browserName) {
        WebDriver driver;
        
        switch (browserName.toLowerCase()) {
            case "chrome":
                driver = createChromeDriver();
                break;
                
            case "firefox":
                driver = createFirefoxDriver();
                break;
                
            case "edge":
                driver = createEdgeDriver();
                break;
                
            default:
                throw new IllegalArgumentException(
                    "Navigateur non supporté : " + browserName + 
                    ". Utilisez chrome, firefox ou edge."
                );
        }
        
        // Configuration commune à tous les navigateurs
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
        
        return driver;
    }
    
    /**
     * Crée un ChromeDriver avec options optimisées
     */
    private static WebDriver createChromeDriver() {
        ChromeOptions options = new ChromeOptions();
        
        // Options pour stabilité et performance
        options.addArguments("--disable-dev-shm-usage");       // Évite crash sur Linux
        options.addArguments("--no-sandbox");                  // Nécessaire en CI/CD
        options.addArguments("--disable-blink-features=AutomationControlled"); // Anti-détection
        options.addArguments("--disable-gpu");                 // Stabilité
        
        // Mode headless (optionnel, activable via config)
        // options.addArguments("--headless=new");
        
        return new ChromeDriver(options);
    }
    
    /**
     * Crée un FirefoxDriver avec options
     */
    private static WebDriver createFirefoxDriver() {
        FirefoxOptions options = new FirefoxOptions();
        
        // Options Firefox
        options.addArguments("--width=1920");
        options.addArguments("--height=1080");
        
        // Mode headless (optionnel)
        // options.addArguments("--headless");
        
        return new FirefoxDriver(options);
    }
    
    /**
     * Crée un EdgeDriver avec options
     */
    private static WebDriver createEdgeDriver() {
        EdgeOptions options = new EdgeOptions();
        
        // Options Edge (similaires à Chrome)
        options.addArguments("--disable-dev-shm-usage");
        options.addArguments("--no-sandbox");
        
        return new EdgeDriver(options);
    }
}

Analyse de l'implémentation :

Élément Rôle Importance
ChromeOptions Configure le comportement de Chrome ⚠️ Critique pour CI/CD (Docker, Jenkins)
--disable-dev-shm-usage Évite crash /dev/shm trop petit (Linux) Nécessaire en environnement conteneurisé
--no-sandbox Désactive sandbox Chrome Obligatoire en CI/CD rootless
maximize() Fenêtre plein écran Évite problèmes d'éléments masqués
implicitlyWait Attente implicite globale Sécurité mais à utiliser avec parcimonie
Pattern Factory : Au lieu de faire new ChromeDriver() partout, on centralise la création dans une Factory. Avantages : - Configuration unique (DRY) - Ajout facile de nouveaux navigateurs - Tests facilement paramétrables (browser via config)
implicitlyWait : À utiliser avec modération. C'est une attente "globale" qui ralentit tous les findElement(). Préférez des waits explicites dans le WebUI (partie suivante).

2.3 ConfigManager : Configuration centralisée

Le ConfigManager lit les paramètres depuis un fichier config.properties. Cela permet de changer l'URL, le navigateur, les timeouts sans toucher au code.

config.properties - Fichier de configuration

# Créer ce fichier dans : src/test/resources/config.properties

# Application
base.url=https://demo.opencart.com
app.name=OpenCart E-commerce

# Navigateur
browser=chrome
headless=false

# Timeouts (en secondes)
timeout.implicit=10
timeout.explicit=20
timeout.pageload=30

# Parallélisation
parallel.enabled=true
thread.count=5

# Reporting
screenshots.onfail=true
screenshots.onpass=false
allure.enabled=true

# Logs
log.level=INFO

ConfigManager.java - Classe complète

package com.ecommerce.config;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * Gestionnaire de configuration centralisé.
 * Lit les propriétés depuis config.properties et les expose via méthodes statiques.
 */
public class ConfigManager {
    
    private static Properties properties;
    private static final String CONFIG_FILE = "src/test/resources/config.properties";
    
    // Bloc static : chargement automatique au démarrage
    static {
        properties = new Properties();
        loadProperties();
    }
    
    /**
     * Charge le fichier de configuration
     */
    private static void loadProperties() {
        try (InputStream input = new FileInputStream(CONFIG_FILE)) {
            properties.load(input);
            System.out.println("✅ Configuration chargée depuis : " + CONFIG_FILE);
        } catch (IOException e) {
            System.err.println("❌ Impossible de charger config.properties : " + e.getMessage());
            throw new RuntimeException("Configuration manquante", e);
        }
    }
    
    /**
     * Récupère une propriété (avec valeur par défaut)
     * @param key clé de la propriété
     * @param defaultValue valeur si clé absente
     * @return valeur de la propriété
     */
    public static String get(String key, String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }
    
    /**
     * Récupère une propriété (lance exception si absente)
     * @param key clé de la propriété
     * @return valeur de la propriété
     */
    public static String get(String key) {
        String value = properties.getProperty(key);
        if (value == null) {
            throw new IllegalArgumentException("Propriété manquante : " + key);
        }
        return value;
    }
    
    // ==================== Méthodes utilitaires ====================
    
    public static String getBaseUrl() {
        return get("base.url");
    }
    
    public static String getBrowser() {
        return get("browser", "chrome");
    }
    
    public static boolean isHeadless() {
        return Boolean.parseBoolean(get("headless", "false"));
    }
    
    public static int getImplicitTimeout() {
        return Integer.parseInt(get("timeout.implicit", "10"));
    }
    
    public static int getExplicitTimeout() {
        return Integer.parseInt(get("timeout.explicit", "20"));
    }
    
    public static int getThreadCount() {
        return Integer.parseInt(get("thread.count", "1"));
    }
    
    public static boolean isScreenshotOnFail() {
        return Boolean.parseBoolean(get("screenshots.onfail", "true"));
    }
    
    public static String getLogLevel() {
        return get("log.level", "INFO");
    }
}

Architecture de la configuration :

  • Bloc static {} : Exécuté automatiquement au chargement de la classe. Le fichier properties est lu une seule fois au démarrage, pas à chaque appel.
  • Méthodes typées : getBaseUrl(), isHeadless() convertissent automatiquement String → type approprié (int, boolean).
  • Valeurs par défaut : Si une propriété manque, on utilise une valeur saine (ex: chrome par défaut) plutôt que crasher.
  • Centralisation : Un seul endroit pour modifier l'URL, le browser, etc. Aucun hardcoding dans les tests.
Pattern Singleton implicite : ConfigManager utilise des méthodes static, il n'y a qu'une seule instance (implicite). Le bloc static garantit un chargement unique thread-safe (garanti par la JVM).

2.4 Intégration : BaseTest avec setup/teardown

Maintenant que nous avons DriverManager, BrowserFactory et ConfigManager, créons la classe BaseTest que tous nos tests vont étendre. Elle initialise le driver avant chaque test et le nettoie après.

BaseTest.java - Classe de base pour tous les tests

package com.ecommerce.tests;

import com.ecommerce.config.ConfigManager;
import com.ecommerce.driver.BrowserFactory;
import com.ecommerce.driver.DriverManager;
import org.openqa.selenium.WebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

/**
 * Classe de base pour tous les tests.
 * Gère le cycle de vie du WebDriver (création/destruction).
 */
public class BaseTest {
    
    /**
     * Exécuté AVANT chaque méthode de test (@Test)
     */
    @BeforeMethod
    public void setUp() {
        System.out.println("\n🚀 Initialisation du test...");
        
        // 1. Lire le navigateur depuis config
        String browser = ConfigManager.getBrowser();
        System.out.println("📱 Navigateur : " + browser);
        
        // 2. Créer le driver via Factory
        WebDriver driver = BrowserFactory.createDriver(browser);
        
        // 3. Stocker dans ThreadLocal via DriverManager
        DriverManager.setDriver(driver);
        
        // 4. Naviguer vers l'URL de base
        String baseUrl = ConfigManager.getBaseUrl();
        driver.get(baseUrl);
        System.out.println("🌐 URL chargée : " + baseUrl);
    }
    
    /**
     * Exécuté APRÈS chaque méthode de test (@Test)
     */
    @AfterMethod
    public void tearDown() {
        System.out.println("🧹 Nettoyage du test...");
        
        // Fermer et nettoyer le driver
        DriverManager.quitDriver();
        
        System.out.println("✅ Test terminé\n");
    }
    
    /**
     * Helper pour récupérer le driver dans les tests
     * @return WebDriver du thread courant
     */
    protected WebDriver getDriver() {
        return DriverManager.getDriver();
    }
}

Fonctionnement du BaseTest :

  1. @BeforeMethod : TestNG exécute cette méthode avant chaque @Test. Équivalent JUnit : @BeforeEach.
  2. Création driver : On lit la config (navigateur), on crée le driver via Factory, on le stocke dans ThreadLocal.
  3. Navigation initiale : Tous les tests démarrent sur la page d'accueil (baseUrl).
  4. @AfterMethod : Exécuté après chaque test, même si le test échoue (finally implicite).
  5. Cleanup : quitDriver() ferme le navigateur et libère la mémoire ThreadLocal.
Tests héritent de BaseTest :
public class LoginTest extends BaseTest {
    @Test
    public void testValidLogin() {
        // Le driver est déjà initialisé par setUp()
        WebDriver driver = getDriver();
        // ... test logic
        // tearDown() sera appelé automatiquement après
    }
}

2.5 Validation du Core Layer

Créons un test simple pour valider que tout fonctionne correctement.

SmokeTest.java - Test de validation

package com.ecommerce.tests;

import org.openqa.selenium.WebDriver;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class SmokeTest extends BaseTest {
    
    @Test
    public void testDriverInitialization() {
        // Le driver doit être initialisé par BaseTest
        WebDriver driver = getDriver();
        
        assertNotNull(driver, "Le driver ne doit pas être null");
        
        String currentUrl = driver.getCurrentUrl();
        System.out.println("URL actuelle : " + currentUrl);
        
        assertTrue(currentUrl.contains("opencart"), 
                  "L'URL doit contenir 'opencart'");
        
        String title = driver.getTitle();
        System.out.println("Titre de la page : " + title);
        
        assertFalse(title.isEmpty(), "Le titre ne doit pas être vide");
    }
}

Pour exécuter ce test :

# Via Maven
mvn clean test -Dtest=SmokeTest

# Via IDE
# Clic droit sur SmokeTest.java → Run As → TestNG Test
Résultat attendu :
🚀 Initialisation du test...
📱 Navigateur : chrome
🌐 URL chargée : https://demo.opencart.com
URL actuelle : https://demo.opencart.com/
Titre de la page : Your Store
🧹 Nettoyage du test...
✅ Test terminé

PASSED: testDriverInitialization
Si le test échoue :
  • Vérifier que config.properties existe dans src/test/resources/
  • Vérifier que Chrome est installé (ou changer browser=firefox)
  • Vérifier les versions dans pom.xml (Selenium 4.35.0)
  • Exécuter mvn clean install pour télécharger les dépendances

Récapitulatif Core Layer

À ce stade, nous avons construit la fondation du framework :

Composant Rôle Pattern
DriverManager Gestion thread-safe du WebDriver ThreadLocal
BrowserFactory Création drivers par navigateur Factory
ConfigManager Configuration centralisée Singleton (implicite)
BaseTest Setup/Teardown automatique Template Method

Prochaine étape : Maintenant que le Core est solide, nous allons créer la couche WebUI Keywords avec toutes les actions réutilisables (click, type, wait, screenshot, etc.).

Partie 3 : WebUI Keywords

Dans cet atelier, nous allons construire une classe WebUI qui encapsule toutes les actions Selenium. C’est le cœur névralgique du framework : chaque test appellera uniquement ses méthodes.

« Plus aucun driver.findElement() dans vos tests ! »

🎯 Objectifs pédagogiques

  • Créer une classe WebUI avec 50+ méthodes réutilisables
  • Remplacer les appels Selenium bruts par des mots-clés centralisés
  • Gérer les waits automatiquement
  • Améliorer la lisibilité et la robustesse des tests

🛠 Étape unique – Implémenter la classe complète

Créez le fichier WebUI.java dans src/main/java/com/ecommerce/keywords/ et copiez le code suivant :

WebUI.java – Code complet (50+ méthodes)

package com.ecommerce.keywords;

import com.ecommerce.driver.DriverManager;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.time.Duration;
import java.util.List;

/**
 * Classe centrale : WebUI Keywords
 * Exemple d’utilisation : WebUI.click(By.id("loginBtn"));
 */
public class WebUI {

    private static final int DEFAULT_TIMEOUT = 20;

    // ==================== NAVIGATION ====================
    public static void openUrl(String url) { DriverManager.getDriver().get(url); }
    public static void back() { DriverManager.getDriver().navigate().back(); }
    public static void forward() { DriverManager.getDriver().navigate().forward(); }
    public static void refresh() { DriverManager.getDriver().navigate().refresh(); }
    public static String getCurrentUrl() { return DriverManager.getDriver().getCurrentUrl(); }
    public static String getTitle() { return DriverManager.getDriver().getTitle(); }

    // ==================== ACTIONS DE BASE ====================
    public static void click(By locator) { waitForClickable(locator).click(); }
    public static void doubleClick(By locator) {
        new Actions(DriverManager.getDriver()).doubleClick(waitForVisible(locator)).perform();
    }
    public static void rightClick(By locator) {
        new Actions(DriverManager.getDriver()).contextClick(waitForVisible(locator)).perform();
    }
    public static void type(By locator, String text) {
        WebElement element = waitForVisible(locator);
        element.clear();
        element.sendKeys(text);
    }
    public static void clearText(By locator) { waitForVisible(locator).clear(); }
    public static String getText(By locator) { return waitForVisible(locator).getText(); }
    public static String getAttribute(By locator, String attr) {
        return waitForVisible(locator).getAttribute(attr);
    }
    public static List<WebElement> getElements(By locator) {
        return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT))
                .until(ExpectedConditions.presenceOfAllElementsLocatedBy(locator));
    }

    // ==================== SELECT (LISTES DÉROULANTES) ====================
    public static void selectByText(By locator, String text) {
        new Select(waitForVisible(locator)).selectByVisibleText(text);
    }
    public static void selectByValue(By locator, String value) {
        new Select(waitForVisible(locator)).selectByValue(value);
    }
    public static void selectByIndex(By locator, int index) {
        new Select(waitForVisible(locator)).selectByIndex(index);
    }

    // ==================== VALIDATIONS ====================
    public static boolean isVisible(By locator) {
        try { waitForVisible(locator, 5); return true; }
        catch (TimeoutException e) { return false; }
    }
    public static boolean isEnabled(By locator) { return waitForVisible(locator).isEnabled(); }
    public static boolean isSelected(By locator) { return waitForVisible(locator).isSelected(); }
    public static void verifyTextEquals(By locator, String expected) {
        if (!getText(locator).equals(expected))
            throw new AssertionError("Texte attendu : " + expected + " mais trouvé : " + getText(locator));
    }
    public static void verifyContains(By locator, String expected) {
        if (!getText(locator).contains(expected))
            throw new AssertionError("Texte ne contient pas : " + expected);
    }

    // ==================== WAITS ====================
    private static WebElement waitForVisible(By locator) { return waitForVisible(locator, DEFAULT_TIMEOUT); }
    private static WebElement waitForVisible(By locator, int seconds) {
        return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(seconds))
            .until(ExpectedConditions.visibilityOfElementLocated(locator));
    }
    private static WebElement waitForClickable(By locator) {
        return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT))
            .until(ExpectedConditions.elementToBeClickable(locator));
    }
    public static void waitForText(By locator, String text) {
        new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT))
            .until(ExpectedConditions.textToBe(locator, text));
    }
    public static void waitForInvisible(By locator) {
        new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT))
            .until(ExpectedConditions.invisibilityOfElementLocated(locator));
    }

    // ==================== ALERTES ====================
    public static void acceptAlert() { DriverManager.getDriver().switchTo().alert().accept(); }
    public static void dismissAlert() { DriverManager.getDriver().switchTo().alert().dismiss(); }
    public static String getAlertText() { return DriverManager.getDriver().switchTo().alert().getText(); }

    // ==================== IFRAMES & WINDOWS ====================
    public static void switchToFrame(By locator) {
        DriverManager.getDriver().switchTo().frame(waitForVisible(locator));
    }
    public static void switchToDefault() { DriverManager.getDriver().switchTo().defaultContent(); }
    public static void switchToWindow(String handle) { DriverManager.getDriver().switchTo().window(handle); }
    public static void closeWindow() { DriverManager.getDriver().close(); }

    // ==================== JAVASCRIPT EXECUTOR ====================
    public static void scrollToElement(By locator) {
        ((JavascriptExecutor) DriverManager.getDriver())
            .executeScript("arguments[0].scrollIntoView(true);", waitForVisible(locator));
    }
    public static void scrollBy(int x, int y) {
        ((JavascriptExecutor) DriverManager.getDriver()).executeScript("window.scrollBy("+x+","+y+");");
    }
    public static void highlight(By locator) {
        ((JavascriptExecutor) DriverManager.getDriver())
            .executeScript("arguments[0].style.border='3px solid red'", waitForVisible(locator));
    }
    public static void clickByJS(By locator) {
        ((JavascriptExecutor) DriverManager.getDriver())
            .executeScript("arguments[0].click();", waitForVisible(locator));
    }

    // ==================== DRAG & DROP ====================
    public static void dragAndDrop(By source, By target) {
        new Actions(DriverManager.getDriver())
            .dragAndDrop(waitForVisible(source), waitForVisible(target))
            .perform();
    }

    // ==================== SCREENSHOTS ====================
    public static String captureScreenshot(String testName) {
        try {
            TakesScreenshot camera = (TakesScreenshot) DriverManager.getDriver();
            File screenshot = camera.getScreenshotAs(OutputType.FILE);
            String path = "screenshots/" + testName + "_" + System.currentTimeMillis() + ".png";
            Files.createDirectories(Paths.get("screenshots"));
            Files.copy(screenshot.toPath(), Paths.get(path));
            return path;
        } catch (IOException e) {
            System.err.println("Erreur screenshot : " + e.getMessage());
            return null;
        }
    }
}

🚀 Étape finale – Cas pratique Login

  1. WebUI.openUrl("https://site.com/login")
  2. WebUI.type(By.id("user"), "admin")
  3. WebUI.type(By.id("pass"), "secret")
  4. WebUI.click(By.id("loginBtn"))
  5. WebUI.verifyTextEquals(By.cssSelector("h1"), "Tableau de bord")
  6. WebUI.captureScreenshot("login_test")
Bravo ! Vous avez désormais un moteur WebUI réutilisable pour tous vos tests UI 🚀

📄 Atelier pratique : Page Objects

Les Page Objects représentent la couche métier d’un framework de test. Ils servent de pont entre vos scénarios et les détails techniques Selenium. L’idée est simple : un test ne doit jamais manipuler un locator directement. Au lieu d’un driver.findElement(), il appelle une méthode métier claire.

« Un test lisible doit ressembler à un scénario métier, pas à une suite de commandes Selenium. »

🎯 Objectifs pédagogiques

  • Centraliser tous les locators dans des classes par page
  • Offrir des méthodes métier explicites : login(), addToCart(), etc.
  • Éviter toute duplication et améliorer la maintenance
  • Faciliter la réutilisation : chaque page devient une brique indépendante

🛠 Étape 1 – Créer la classe de base

La BasePage est la superclasse de toutes vos pages. Elle fournit des méthodes communes pour la navigation, les validations génériques et la gestion de l’affichage.

BasePage.java

package com.ecommerce.pages;

import com.ecommerce.keywords.WebUI;
import org.openqa.selenium.By;

public class BasePage {

    // Navigation
    public void openUrl(String url) { WebUI.openUrl(url); }
    public String getCurrentUrl() { return WebUI.getCurrentUrl(); }
    public String getTitle() { return WebUI.getTitle(); }
    public void refreshPage() { WebUI.refresh(); }
    public void back() { WebUI.back(); }
    public void forward() { WebUI.forward(); }

    // Vérifications génériques
    public boolean isTextPresent(String text) {
        By body = By.tagName("body");
        return WebUI.getText(body).contains(text);
    }

    public boolean isElementVisible(By locator) {
        return WebUI.isVisible(locator);
    }

    public String getElementText(By locator) {
        return WebUI.getText(locator);
    }

    // Utilitaires
    public void takeScreenshot(String testName) {
        WebUI.captureScreenshot(testName);
    }

    public void highlightElement(By locator) {
        WebUI.highlight(locator);
    }

    public void scrollTo(By locator) {
        WebUI.scrollToElement(locator);
    }
}

Cette classe agit comme une boîte à outils de base. Toutes les autres pages héritent de ces fonctionnalités et n’ont pas besoin de réécrire ces utilitaires.

🛠 Étape 2 – Créer le LoginPage

La page de connexion est un exemple classique pour illustrer le pattern Page Object. Nous allons y inclure :

  • les locators des champs et boutons
  • une méthode login() réutilisable
  • des méthodes de vérification (erreurs, validations)
  • des helpers pour tester des cas limites (champs vides, mauvais mot de passe)

LoginPage.java

package com.ecommerce.pages;

import com.ecommerce.keywords.WebUI;
import org.openqa.selenium.By;

public class LoginPage extends BasePage {

    // Locators
    private final By inputUsername = By.id("user");
    private final By inputPassword = By.id("pass");
    private final By btnLogin = By.id("loginBtn");
    private final By lblError = By.cssSelector(".error-msg");
    private final By lblWelcome = By.cssSelector("h1");

    // Méthodes métier
    public void login(String username, String password) {
        WebUI.type(inputUsername, username);
        WebUI.type(inputPassword, password);
        WebUI.click(btnLogin);
    }

    public void loginWithEmptyFields() {
        WebUI.click(btnLogin);
    }

    public void loginWithOnlyUsername(String username) {
        WebUI.type(inputUsername, username);
        WebUI.click(btnLogin);
    }

    public void loginWithOnlyPassword(String password) {
        WebUI.type(inputPassword, password);
        WebUI.click(btnLogin);
    }

    // Vérifications
    public boolean isErrorVisible() {
        return WebUI.isVisible(lblError);
    }

    public String getErrorMessage() {
        return WebUI.getText(lblError);
    }

    public boolean isWelcomeMessageVisible() {
        return WebUI.isVisible(lblWelcome);
    }

    public String getWelcomeMessage() {
        return WebUI.getText(lblWelcome);
    }
}

📊 Comparaison avant/après Page Object

Sans Page Object Avec Page Object
// Test brut
WebUI.openUrl("https://site.com/login");
WebUI.type(By.id("user"), "admin");
WebUI.type(By.id("pass"), "secret");
WebUI.click(By.id("loginBtn"));
String msg = WebUI.getText(By.cssSelector("h1"));
assertEquals(msg, "Tableau de bord");
// Test lisible
LoginPage loginPage = new LoginPage();
loginPage.openUrl("https://site.com/login");
loginPage.login("admin", "secret");
assertTrue(loginPage.isWelcomeMessageVisible());
Code verbeux, locators partout Code lisible, isolé dans la classe LoginPage

🛠 Étape 3 – Un autre exemple : ProductPage

Sur la page produit, on va gérer l’ajout au panier. Ce Page Object montre bien comment encapsuler plusieurs actions métier.

ProductPage.java

package com.ecommerce.pages;

import com.ecommerce.keywords.WebUI;
import org.openqa.selenium.By;

public class ProductPage extends BasePage {

    // Locators
    private final By lblProductName = By.cssSelector("h1.product-title");
    private final By btnAddToCart = By.id("add-to-cart");
    private final By lblConfirmation = By.cssSelector(".cart-confirmation");

    // Méthodes métier
    public String getProductName() {
        return WebUI.getText(lblProductName);
    }

    public void addToCart() {
        WebUI.click(btnAddToCart);
    }

    public boolean isConfirmationVisible() {
        return WebUI.isVisible(lblConfirmation);
    }
}

🚀 Étape finale – Cas pratique

Voici un scénario complet qui combine LoginPage et ProductPage.

ShopTest.java

package com.ecommerce.tests;

import com.ecommerce.pages.LoginPage;
import com.ecommerce.pages.ProductPage;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class ShopTest extends BaseTest {

    @Test
    public void testAddProductAfterLogin() {
        LoginPage loginPage = new LoginPage();
        ProductPage productPage = new ProductPage();

        // Étape 1 : Login
        loginPage.openUrl("https://site.com/login");
        loginPage.login("admin", "secret");
        assertTrue(loginPage.isWelcomeMessageVisible());

        // Étape 2 : Aller sur une page produit
        productPage.openUrl("https://site.com/product/123");
        String name = productPage.getProductName();
        System.out.println("Produit testé : " + name);

        // Étape 3 : Ajouter au panier
        productPage.addToCart();
        assertTrue(productPage.isConfirmationVisible(), "Le produit n'a pas été ajouté !");
    }
}
Conclusion : Avec les Page Objects, vos tests UI deviennent lisibles, robustes et maintenables. Les changements de locators n’affectent plus vos tests : ils se gèrent dans une seule classe.

📦 Partie 5 : Builders & Data – Génération et gestion de données de test

Un framework de test solide ne doit pas dépendre de données codées en dur. Les Builders et les Data Factories permettent de générer des données dynamiques, réalistes et contrôlées. L’objectif est simple : plus de login "admin/admin" en dur dans les tests.

« Vos tests doivent être indépendants des données fixes. Si demain la base change, les tests doivent toujours passer. »

🎯 Objectifs pédagogiques

  • Éviter le hardcoding des données
  • Utiliser un Builder Pattern pour créer des objets métier (User, Product, Order)
  • Générer des données réalistes avec JavaFaker
  • Centraliser la logique de création de données dans un package dédié

🛠 Étape 1 – Créer une entité métier

Exemple avec un utilisateur. Nous commençons par définir la classe User en mode immuable.

User.java

package com.ecommerce.data;

public class User {

    private final String username;
    private final String password;
    private final String email;

    // Constructeur privé → utilisation via Builder uniquement
    private User(UserBuilder builder) {
        this.username = builder.username;
        this.password = builder.password;
        this.email = builder.email;
    }

    // Getters
    public String getUsername() { return username; }
    public String getPassword() { return password; }
    public String getEmail() { return email; }

    // Builder interne
    public static class UserBuilder {
        private String username;
        private String password;
        private String email;

        public UserBuilder username(String username) {
            this.username = username;
            return this;
        }

        public UserBuilder password(String password) {
            this.password = password;
            return this;
        }

        public UserBuilder email(String email) {
            this.email = email;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

Ici, on applique le Builder Pattern. Avantage : plus de constructeur à rallonge, et possibilité de créer des variantes facilement.

🛠 Étape 2 – Ajouter une DataFactory

La DataFactory centralise la création de données. Elle peut générer des objets soit fixes (prévisibles pour des tests précis), soit aléatoires (pour tester la robustesse).

UserFactory.java

package com.ecommerce.data;

import com.github.javafaker.Faker;

public class UserFactory {

    private static final Faker faker = new Faker();

    // Utilisateur par défaut (utile pour login de test)
    public static User defaultUser() {
        return new User.UserBuilder()
                .username("admin")
                .password("secret")
                .email("admin@site.com")
                .build();
    }

    // Utilisateur aléatoire
    public static User randomUser() {
        return new User.UserBuilder()
                .username(faker.name().username())
                .password(faker.internet().password(8, 12))
                .email(faker.internet().emailAddress())
                .build();
    }

    // Utilisateur avec données spécifiques
    public static User customUser(String username, String password, String email) {
        return new User.UserBuilder()
                .username(username)
                .password(password)
                .email(email)
                .build();
    }
}

📊 Tableau comparatif – Approches possibles

Méthode Avantages Inconvénients
Données en dur (admin/admin) Simple à écrire ❌ fragile, ❌ difficile à maintenir
Builder + Factory ✅ Flexible, ✅ maintenable, ✅ lisible Légèrement plus de code à écrire
JavaFaker aléatoire ✅ Données réalistes, ✅ pas de duplication ❌ résultats variables (pas toujours prédictibles)

🛠 Étape 3 – Intégration dans les tests

Dans vos tests, vous pouvez désormais créer des utilisateurs réalistes en une ligne.

LoginTest.java

package com.ecommerce.tests;

import com.ecommerce.data.User;
import com.ecommerce.data.UserFactory;
import com.ecommerce.pages.LoginPage;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class LoginTest extends BaseTest {

    @Test
    public void testLoginWithDefaultUser() {
        LoginPage loginPage = new LoginPage();
        User user = UserFactory.defaultUser();

        loginPage.openUrl("https://site.com/login");
        loginPage.login(user.getUsername(), user.getPassword());

        assertTrue(loginPage.isWelcomeMessageVisible(), "Le login a échoué !");
    }

    @Test
    public void testLoginWithRandomUser() {
        LoginPage loginPage = new LoginPage();
        User random = UserFactory.randomUser();

        loginPage.openUrl("https://site.com/login");
        loginPage.login(random.getUsername(), random.getPassword());

        assertTrue(loginPage.isErrorVisible(), "Un user aléatoire ne devrait pas se connecter !");
    }
}

🚀 Étape finale – Cas pratique

Construisons un scénario de bout en bout avec des données dynamiques : création d’un utilisateur → login → ajout d’un produit au panier.

ScenarioUserJourney.java

package com.ecommerce.tests;

import com.ecommerce.data.User;
import com.ecommerce.data.UserFactory;
import com.ecommerce.pages.LoginPage;
import com.ecommerce.pages.ProductPage;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class ScenarioUserJourney extends BaseTest {

    @Test
    public void testUserJourney() {
        // 1. Génération utilisateur
        User user = UserFactory.randomUser();

        // 2. Login
        LoginPage loginPage = new LoginPage();
        loginPage.openUrl("https://site.com/login");
        loginPage.login(user.getUsername(), user.getPassword());
        assertTrue(loginPage.isErrorVisible(), "Connexion d'un user inconnu doit échouer");

        // 3. Scénario avec un user par défaut (admin)
        User admin = UserFactory.defaultUser();
        loginPage.login(admin.getUsername(), admin.getPassword());
        assertTrue(loginPage.isWelcomeMessageVisible(), "Admin doit se connecter");

        // 4. Ajout d'un produit au panier
        ProductPage productPage = new ProductPage();
        productPage.openUrl("https://site.com/product/123");
        productPage.addToCart();
        assertTrue(productPage.isConfirmationVisible(), "Le produit n'a pas été ajouté !");
    }
}
Conclusion : Grâce aux Builders et Factories, vos tests ne dépendent plus de données fixes. Ils deviennent réutilisables, flexibles et robustes. Un nouveau test = un nouveau jeu de données !

📊 Partie 6 : Listeners & Reports – Suivi et reporting des tests

Un framework de test professionnel doit fournir une visibilité totale sur l’exécution. C’est le rôle des Listeners et des Reports : écouter les événements TestNG (succès, échec, skip) et produire des rapports lisibles (Allure, logs, captures).

« Un test qui échoue sans log ni capture n’a aucune valeur. Un test qui échoue avec un rapport clair devient une opportunité d’amélioration. »

🎯 Objectifs pédagogiques

  • Mettre en place un TestListener qui capture logs et screenshots
  • Brancher un système de logs Log4j2
  • Générer des rapports modernes avec Allure
  • Standardiser les logs : info, warning, error
  • Donner de la valeur métier aux rapports (pas que du technique)

🛠 Étape 1 – Créer un Listener TestNG

Un listener implémente ITestListener de TestNG. Il réagit à chaque étape d’un test : démarrage, succès, échec, skip.

TestListener.java

package com.ecommerce.listeners;

import com.ecommerce.driver.DriverManager;
import com.ecommerce.keywords.WebUI;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class TestListener implements ITestListener {

    @Override
    public void onTestStart(ITestResult result) {
        System.out.println("🚀 Test démarré : " + result.getName());
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        System.out.println("✅ Test réussi : " + result.getName());
    }

    @Override
    public void onTestFailure(ITestResult result) {
        System.err.println("❌ Test échoué : " + result.getName());

        // Capture screenshot en cas d’échec
        String screenshot = WebUI.captureScreenshot(result.getName());
        System.err.println("📸 Screenshot enregistré : " + screenshot);
    }

    @Override
    public void onTestSkipped(ITestResult result) {
        System.out.println("⚠️ Test ignoré : " + result.getName());
    }

    @Override
    public void onStart(ITestContext context) {
        System.out.println("=== Suite démarrée : " + context.getName() + " ===");
    }

    @Override
    public void onFinish(ITestContext context) {
        System.out.println("=== Suite terminée : " + context.getName() + " ===");
    }
}

🛠 Étape 2 – Activer le Listener

Il faut déclarer le listener dans le fichier testng.xml ou via l’annotation @Listeners.

testng.xml

<suite name="Ecommerce Suite">
  <listeners>
    <listener class-name="com.ecommerce.listeners.TestListener"/>
  </listeners>
  <test name="SmokeTests">
    <classes>
      <class name="com.ecommerce.tests.LoginTest"/>
    </classes>
  </test>
</suite>

🛠 Étape 3 – Intégrer Log4j2

Plutôt que d’utiliser System.out.println(), on branche Log4j2 pour un logging pro : niveaux, couleurs, fichiers séparés.

log4j2.xml (dans src/test/resources)

<Configuration status="INFO">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="[%d{HH:mm:ss}] [%p] %c - %m%n"/>
    </Console>
    <File name="FileLog" fileName="logs/test.log">
      <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%p] %c - %m%n"/>
    </File>
  </Appenders>
  <Loggers>
    <Root level="info">
      <AppenderRef ref="Console"/>
      <AppenderRef ref="FileLog"/>
    </Root>
  </Loggers>
</Configuration>

Ensuite, on injecte Log4j2 dans le Listener :

TestListener.java (avec Log4j2)

private static final Logger logger = LogManager.getLogger(TestListener.class);

@Override
public void onTestStart(ITestResult result) {
    logger.info("🚀 Test démarré : {}", result.getName());
}

@Override
public void onTestFailure(ITestResult result) {
    logger.error("❌ Test échoué : {}", result.getName());
    String screenshot = WebUI.captureScreenshot(result.getName());
    logger.error("📸 Screenshot enregistré : {}", screenshot);
}

🛠 Étape 4 – Intégrer Allure Reports

Allure fournit des rapports modernes : étapes, logs, captures, graphes. On ajoute la dépendance dans pom.xml :

Dépendance Allure

<dependency>
  <groupId>io.qameta.allure</groupId>
  <artifactId>allure-testng</artifactId>
  <version>2.29.1</version>
</dependency>

Exemple d’intégration dans un test avec des annotations Allure :

LoginTest.java (avec Allure)

@Epic("Authentification")
@Feature("Login")
@Story("Connexion avec utilisateur valide")
@Test(description = "Vérifie que l'admin peut se connecter")
public void testValidLogin() {
    LoginPage loginPage = new LoginPage();
    User admin = UserFactory.defaultUser();

    Allure.step("Ouvrir la page de login");
    loginPage.openUrl("https://site.com/login");

    Allure.step("Saisir identifiants");
    loginPage.login(admin.getUsername(), admin.getPassword());

    Allure.step("Vérifier tableau de bord affiché");
    assertTrue(loginPage.isWelcomeMessageVisible());
}

📊 Tableau comparatif des rapports

Système Avantages Inconvénients
Logs console Rapide, simple Peu lisible sur de gros projets
Log4j2 Centralisé, configurable, fichiers séparés Doit être bien configuré
Allure Rapports modernes avec captures, étapes et graphes Nécessite un plugin Maven + viewer

🚀 Étape finale – Cas pratique

Voici un test complet qui combine Listener + Log4j2 + Allure.

CheckoutTest.java

package com.ecommerce.tests;

import com.ecommerce.data.UserFactory;
import com.ecommerce.pages.LoginPage;
import com.ecommerce.pages.ProductPage;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
import io.qameta.allure.*;

@Epic("E-commerce")
@Feature("Checkout")
public class CheckoutTest extends BaseTest {

    @Test(description = "Scénario e2e : login + ajout produit + vérification panier")
    @Severity(SeverityLevel.CRITICAL)
    @Story("Achat produit depuis la page login")
    public void testCheckoutFlow() {
        LoginPage loginPage = new LoginPage();
        ProductPage productPage = new ProductPage();

        step("Se connecter en tant qu'admin");
        loginPage.openUrl("https://site.com/login");
        loginPage.login(UserFactory.defaultUser().getUsername(),
                        UserFactory.defaultUser().getPassword());
        assertTrue(loginPage.isWelcomeMessageVisible());

        step("Ajouter un produit au panier");
        productPage.openUrl("https://site.com/product/123");
        productPage.addToCart();
        assertTrue(productPage.isConfirmationVisible());

        step("Capturer un screenshot");
        WebUI.captureScreenshot("checkout_flow");
    }

    private void step(String message) {
        Allure.step(message);
    }
}
Conclusion : Avec les Listeners, Log4j2 et Allure, vos tests deviennent observables. Chaque échec produit un log détaillé, un screenshot et un rapport consultable par toute l’équipe. Plus jamais un test « qui échoue sans explication » !

Partie 7 : Tests complets – Scénarios End-to-End

C’est ici que tout ce que nous avons construit prend vie. Après avoir mis en place le Core Layer, WebUI, les Page Objects, les Builders et les Listeners, nous pouvons écrire des scénarios complets de bout en bout (E2E) qui simulent le parcours utilisateur réel : de la connexion jusqu’à la validation d’une commande.

« Les tests E2E sont le reflet le plus proche du parcours métier. Ils valident non seulement la technique, mais surtout l’expérience utilisateur. »

7.1 Objectifs pédagogiques

  • Assembler toutes les couches du framework dans un test complet
  • Écrire des scénarios robustes, lisibles et maintenables
  • Utiliser les Page Objects pour séparer logique métier et technique
  • Intégrer la gestion des données (Builders & Factories)
  • Vérifier la traçabilité via Allure Reports

7.2 Exemple – Login Test (valide & invalide)

LoginTest.java

package com.ecommerce.tests;

import com.ecommerce.data.User;
import com.ecommerce.data.UserFactory;
import com.ecommerce.pages.LoginPage;
import com.ecommerce.pages.DashboardPage;
import io.qameta.allure.*;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

@Epic("Authentification")
@Feature("Login")
public class LoginTest extends BaseTest {

    @Story("Connexion avec utilisateur valide")
    @Test(description = "Vérifie que l'admin peut se connecter avec succès")
    public void testValidLogin() {
        LoginPage loginPage = new LoginPage();
        User admin = UserFactory.defaultUser();

        Allure.step("Ouvrir la page de login");
        loginPage.openUrl("https://site.com/login");

        Allure.step("Saisir identifiants");
        DashboardPage dashboard = loginPage.login(admin.getUsername(), admin.getPassword());

        Allure.step("Vérifier que le tableau de bord est visible");
        assertTrue(dashboard.isWelcomeMessageVisible(), 
            "Le message de bienvenue doit être affiché");
    }

    @Story("Connexion avec mot de passe incorrect")
    @Test(description = "Vérifie qu'un login invalide est rejeté")
    public void testInvalidLogin() {
        LoginPage loginPage = new LoginPage();

        Allure.step("Ouvrir la page de login");
        loginPage.openUrl("https://site.com/login");

        Allure.step("Saisir identifiants incorrects");
        loginPage.login("fakeUser", "wrongPass");

        Allure.step("Vérifier que le message d'erreur apparaît");
        assertTrue(loginPage.isErrorMessageVisible(),
            "Un message d'erreur doit apparaître");
    }
}

7.3 Exemple – Checkout Test (scénario complet)

Ce scénario illustre un vrai parcours utilisateur : rechercher un produit, l’ajouter au panier, passer commande, puis vérifier la confirmation.

CheckoutTest.java

package com.ecommerce.tests;

import com.ecommerce.data.User;
import com.ecommerce.data.UserFactory;
import com.ecommerce.pages.*;
import io.qameta.allure.*;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

@Epic("E-commerce")
@Feature("Commande")
public class CheckoutTest extends BaseTest {

    @Story("Parcours complet d'achat")
    @Test(description = "Effectue une commande depuis la recherche jusqu'au paiement")
    public void testCompleteCheckout() {
        User customer = UserFactory.randomUser();

        // Login
        LoginPage loginPage = new LoginPage();
        loginPage.openUrl("https://site.com/login");
        DashboardPage dashboard = loginPage.login(customer.getUsername(), customer.getPassword());
        assertTrue(dashboard.isWelcomeMessageVisible());

        // Recherche produit
        SearchPage search = dashboard.goToSearch();
        search.search("Laptop");
        assertTrue(search.isResultVisible("Laptop Pro"));

        // Ajout panier
        ProductPage product = search.selectProduct("Laptop Pro");
        product.addToCart();

        CartPage cart = product.goToCart();
        assertTrue(cart.containsProduct("Laptop Pro"));

        // Checkout
        CheckoutPage checkout = cart.proceedToCheckout();
        checkout.enterAddress("123 Rue de Paris", "75000", "Paris");
        checkout.selectPayment("Carte Bancaire");
        ConfirmationPage confirmation = checkout.placeOrder();

        // Vérification
        assertTrue(confirmation.isOrderConfirmed(), 
            "La commande doit être confirmée");
        assertEquals(confirmation.getOrderTotal(), cart.getTotal(), 
            "Le montant doit correspondre");
    }
}

7.4 DataProviders – Multiplication des jeux de données

Les @DataProvider TestNG permettent d’exécuter le même scénario avec plusieurs utilisateurs ou produits. Cela augmente la couverture des tests sans duplication de code.

LoginDataTest.java

package com.ecommerce.tests;

import com.ecommerce.data.User;
import com.ecommerce.data.UserFactory;
import com.ecommerce.pages.LoginPage;
import io.qameta.allure.*;
import org.testng.annotations.*;

import static org.testng.Assert.*;

public class LoginDataTest extends BaseTest {

    @DataProvider(name = "users")
    public Object[][] users() {
        return new Object[][] {
            { UserFactory.defaultUser() },
            { UserFactory.randomUser() },
            { new User("guest", "guest123") }
        };
    }

    @Test(dataProvider = "users")
    public void testMultipleLogins(User user) {
        LoginPage loginPage = new LoginPage();
        loginPage.openUrl("https://site.com/login");
        loginPage.login(user.getUsername(), user.getPassword());

        assertTrue(loginPage.isLoginAttempted(), 
            "La tentative de login doit être effectuée");
    }
}

7.5 Suites de tests – Organisation

Les tests sont regroupés en suites pour exécuter des sous-ensembles adaptés :

Suite Contenu Usage
smoke.xml LoginTest, SmokeTest Validation rapide (CI/CD)
regression.xml Tous les tests fonctionnels Exécution quotidienne
e2e.xml CheckoutTest + parcours critiques Avant release majeure

7.6 Schéma – Flux d’exécution


[TestNG Suite] 
   └── BaseTest (setup/teardown)
         ├── LoginTest
         │     └── LoginPage → DashboardPage
         ├── CheckoutTest
         │     └── LoginPage → DashboardPage → SearchPage → ProductPage
         │         → CartPage → CheckoutPage → ConfirmationPage
         └── LoginDataTest
               └── DataProvider → LoginPage
  

7.7 Checklist finale

  • ✅ Tous les tests passent en local avec mvn clean test
  • ✅ Les scénarios couvrent login, panier, checkout
  • ✅ Allure génère un rapport lisible (allure serve target/allure-results)
  • ✅ Les suites TestNG sont définies (smoke, regression, e2e)
  • ✅ Les données de test sont générées via Factories (pas hardcodées)
  • ✅ Les screenshots s’ajoutent automatiquement en cas d’échec
Conclusion : Cette dernière étape valide que votre framework est production-ready. Vous pouvez l’intégrer en CI/CD et l’étendre avec de nouveaux scénarios métiers.

💻 Exercices – Mode entraînement

Ici, pas de code fourni. Chaque exercice propose un énoncé et des résultats attendus (révélés via une liste déroulante). L’objectif est de vous faire pratiquer le framework construit dans l’atelier.

Conseil : commencez par formaliser vos critères d’acceptation (Given/When/Then) avant de coder.
Contexte : Tous les exercices utilisent le site OpenCart Demo, qui servira de terrain de jeu commun pour tester les fonctionnalités (login, recherche, panier, checkout, etc.).

1. WebUI

1.1 – Clic robuste sur élément masqué (overlay)

Un bouton « Commander » devient cliquable après disparition d’un loader. Écrivez un test qui n’échoue jamais à cause de l’overlay.

Résultats attendus
  • Le test attend explicitement la disparition du loader.
  • Le bouton « Commander » est effectivement cliqué.
  • Aucun flakiness observé en 5 exécutions consécutives.

1.2 – Défilement et apparition lazy-load

La liste des produits se charge par paquets en scrollant. Validez que le dernier item devient visible après défilement.

Résultats attendus
  • Le dernier produit est présent dans le DOM et visible à l’écran.
  • Aucun timeout de visibilité.
  • La page n’est pas rafraîchie (défilement seulement).

1.3 – Sélection dans un <select>

Sélectionnez la valeur « France » dans un select pays et vérifiez que la TVA affichée est mise à jour.

Résultats attendus
  • Le pays affiché est « France ».
  • Le label de TVA passe à la valeur attendue pour FR.
  • Aucune erreur JS en console (si vous la lisez).

1.4 – Alerte navigateur (confirm)

Une suppression d’article ouvre une alerte de confirmation. Validez les deux branches : accepter et annuler.

Résultats attendus
  • Accepter → l’article disparaît de la liste.
  • Annuler → la liste reste inchangée.
  • Le texte de l’alerte est conforme au cahier des charges.

1.5 – iFrame & champ interne

Renseignez un champ « Description » situé dans un iFrame et validez que la prévisualisation (hors iFrame) se met à jour.

Résultats attendus
  • Le focus passe correctement dans l’iFrame, puis revient au contexte principal.
  • La prévisualisation hors iFrame reflète le texte saisi.
  • Aucun stale element après le switch de contexte.

1.6 – Drag & Drop (réordonner)

Réordonnez une liste de favoris par glisser-déposer et validez que l’ordre est persisté côté UI.

Résultats attendus
  • L’élément déplacé change bien d’index.
  • L’ordre final correspond à l’attendu métier.
  • Le nouvel ordre subsiste après refresh.

2. Page Objects

2.1 – LoginPage fluide

Rendez vos méthodes de LoginPage chaînables et exposez une méthode métier unique login().

Résultats attendus
  • Un appel unique permet de se connecter (pas de Selenium brut dans le test).
  • Retour d’une page suivante (Dashboard) après succès.
  • Message d’erreur visible en cas d’échec, sans exception non gérée.

2.2 – ProductPage variantes

Gérez les variantes (taille/couleur) d’un produit et validez la variation de prix associée.

Résultats attendus
  • La combinaison taille+couleur choisie est affichée.
  • Le prix affiché correspond à la variante.
  • Le bouton « Ajouter au panier » reste actif.

2.3 – Navigation Pages

Depuis DashboardPage, naviguez vers SearchPage puis ProductPage via des méthodes métier (pas d’URL en dur dans le test).

Résultats attendus
  • Les transitions de pages utilisent uniquement des méthodes de Page Objects.
  • Chaque page possède une méthode isLoaded() fiable.
  • Le test ne connaît aucun locator.

3. Builders & Data

3.1 – Utilisateur aléatoire contrôlé

Générez un utilisateur avec email valide et mot de passe entre 10 et 14 caractères, incluant au moins un chiffre.

Résultats attendus
  • L’email respecte le format RFC basique (xxx@yyy.zz).
  • Le mot de passe respecte la contrainte de longueur + 1 chiffre minimum.
  • Les valeurs sont loguées dans le rapport (sans afficher le mot de passe complet si politique).

3.2 – ProductBuilder avec options

Construisez un Product avec des champs obligatoires (nom, prix) et optionnels (remise, tags). Validez la remise calculée.

Résultats attendus
  • Un produit par défaut est créable sans options.
  • Le prix final tient compte de la remise si fournie.
  • Les tags (liste) sont optionnels et affichés correctement.

3.3 – DataProvider utilisateurs

Paramétrez un test de login avec un DataProvider : admin valide, user inconnu, user valide mais inactif.

Résultats attendus
  • 3 itérations distinctes visibles dans le rapport.
  • Résultats conformes : succès / erreur / message « compte inactif ».
  • Pas de duplication de code dans le test.

4. Listeners & Reports

4.1 – Screenshot auto en échec

Provoquez un échec contrôlé et vérifiez que le screenshot est attaché au rapport et enregistré sur disque.

Résultats attendus
  • Un fichier image existe dans le dossier prévu (ex: screenshots/).
  • Le rapport (Allure) affiche la capture en pièce jointe du test en échec.
  • Le chemin du fichier est logué.

4.2 – Étapes Allure métier

Ajoutez des étapes métier (« Rechercher produit », « Ajouter au panier », etc.) et validez leur présence dans le rapport.

Résultats attendus
  • Chaque étape est visible dans la timeline du test.
  • Les descriptions sont lisibles (pas de jargon technique).
  • Les niveaux de sévérité sont pertinents (BLOCKER/CRITICAL/etc.).

4.3 – Logs propres (Log4j2)

Assurez-vous que les logs génèrent un fichier logs/test.log avec un format homogène.

Résultats attendus
  • Le fichier de logs est créé et rempli.
  • Le pattern contient date, niveau, catégorie, message.
  • Pas de System.out laissé dans le code de prod des tests.

5. Tests complets

5.1 – Parcours E2E standard

Login réussi → recherche produit → ajout panier → checkout → confirmation.

Résultats attendus
  • Chaque page dispose d’un isLoaded() fiable.
  • Le total du panier correspond au total de confirmation.
  • Le test joint au moins un screenshot dans le rapport.

5.2 – Parcours négatif (rupture)

Tentez d’acheter un article en rupture : le système doit refuser la commande proprement.

Résultats attendus
  • Message clair « Rupture de stock » affiché.
  • Aucun débit/paiement simulé.
  • Retour vers la page produit ou panier avec état cohérent.

5.3 – Filtres & tri

Appliquez un filtre de prix et un tri par pertinence, puis validez l’ordre des résultats.

Résultats attendus
  • Tous les produits affichés respectent la fourchette de prix.
  • L’ordre correspond au critère de tri choisi.
  • Le filtre reste actif après navigation avant/arrière.

6. Bonus & Défis

6.1 – Robustesse anti-flaky

Rendez un test instable (popups sporadiques, latence aléatoire) stable via vos utilitaires.

Résultats attendus
  • Le test passe 10 fois d’affilée sans échec.
  • Les waits sont centralisés (pas de duplication sauvage).
  • Les popups sont gérées proprement (fermeture ou contournement).

6.2 – Accessibilité (a11y) minimum

Vérifiez l’accessibilité minimale : présence d’attributs alt pour les images de produit et focus clavier sur les CTA.

Résultats attendus
  • Toutes les images produit ont un alt non vide.
  • Les boutons principaux sont atteignables au clavier.
  • Les éléments focusables ont une indication visible de focus.
Fin de la série : ces exercices complètent l’atelier en vous forçant à pratiquer chaque couche du framework sans « code-moule ».

❌ 50+ erreurs Java avancé courantes en QA

Ce catalogue recense les erreurs les plus fréquentes liées à l’usage de Java avancé dans la construction de frameworks de tests automatisés. Ces pièges apparaissent dès que l’on combine multi-threading, Streams, Reflection, Generics et intégration aux outils de QA (Selenium, Appium, API clients).

Chaque carte identifie un anti-pattern avec son symptôme, sa cause, et une solution pratique. Les erreurs sont organisées en 9 catégories :

  • Concurrency & Threading — ThreadLocal, ExecutorService, parallélisme TestNG
  • Streams & Collections — lazy eval, collectors, side effects
  • Exceptions & Gestion d’erreurs — checked, unchecked, try-with-resources
  • Generics & Reflection — factories, type safety, dynamic loading
  • Données & Builders — immutabilité, JSON, pattern Builder
  • APIs & IO — fichiers, HTTP clients, sérialisation
  • Performance & Mémoire — leaks, GC, objets lourds
  • Tests & Framework Integration — TestNG/JUnit, annotations, lifecycle
  • Patterns avancés appliqués QA — Singleton, Proxy, Adapter

🧵 Catégorie 1 : Concurrency & Threading

❌ 1. WebDriver static partagé

Symptôme : Tests parallèles s’écrasent.

Cause : Variable static globale.

Solution : ThreadLocal<WebDriver>.

❌ 2. Oublier driver.remove()

Symptôme : OutOfMemory après 100 tests.

Cause : ThreadLocal non nettoyé.

Solution : Appeler remove() en teardown.

❌ 3. ExecutorService jamais shutdown

Symptôme : JVM qui ne termine pas.

Cause : Threads vivants.

Solution : shutdown() / shutdownNow().

❌ 4. Collections non thread-safe

Symptôme : Données corrompues en parallèle.

Cause : ArrayList partagé.

Solution : ConcurrentHashMap, CopyOnWriteArrayList.

❌ 5. Future.get() bloquant

Symptôme : Test qui freeze.

Cause : get() sans timeout.

Solution : get(timeout) + gestion exception.

❌ 6. Mauvaise config TestNG parallel

Symptôme : @BeforeClass partagés.

Cause : Setup global.

Solution : Isoler par thread.

🌊 Catégorie 2 : Streams & Collections

❌ 7. Stream consommé deux fois

Symptôme : IllegalStateException.

Cause : Réutilisation d’un stream.

Solution : Recréer ou collecter.

❌ 8. Side effects dans forEach()

Symptôme : Résultats non déterministes.

Cause : Variables externes modifiées.

Solution : Streams purs.

❌ 9. Collectors.toList() mutable

Symptôme : List modifiée ailleurs.

Cause : Réutilisation mutable.

Solution : toUnmodifiableList().

❌ 10. parallelStream() abusif

Symptôme : Tests instables/lents.

Cause : Threads inutiles.

Solution : Parallel seulement si CPU-bound.

❌ 11. Comparator non cohérent

Symptôme : Tri incohérent.

Cause : Comparator non transitif.

Solution : Respecter contrat compare().

❌ 12. Utiliser removeIf() sur liste partagée

Symptôme : ConcurrentModificationException.

Cause : Modification concurrente.

Solution : CopyOnWriteArrayList.

⚡ Catégorie 3 : Exceptions & Gestion d’erreurs

❌ 13. Swallow d’exception

Symptôme : Test “passe” alors qu’il échoue.

Cause : catch(Exception e) {}

Solution : Logger + fail.

❌ 14. Checked non gérées

Symptôme : Compilation impossible.

Cause : IOException ignorée.

Solution : try-with-resources.

❌ 15. finally masque exception

Symptôme : Assertion jamais exécutée.

Cause : return dans finally.

Solution : Jamais de return dans finally.

❌ 16. TimeoutException mal gérée

Symptôme : Flaky tests.

Cause : Catch générique.

Solution : Catch ciblé.

❌ 17. Assertions dans try-catch

Symptôme : Test masqué.

Cause : assertTrue() dans catch.

Solution : Assertions hors gestion erreur.

🔮 Catégorie 4 : Generics & Reflection

❌ 18. Raw types

Symptôme : Warnings partout.

Cause : List au lieu de List<User>.

Solution : Utiliser generics typés.

❌ 19. Casts dangereux

Symptôme : ClassCastException runtime.

Cause : Mauvais generic.

Solution : Type-safe factories.

❌ 20. Reflection sans contrôle

Symptôme : Tests cassent après refactor.

Cause : getDeclaredField() fragile.

Solution : Encapsulation via API publique.

❌ 21. Mauvaise utilisation Optional

Symptôme : NullPointer malgré Optional.

Cause : get() sans isPresent().

Solution : ifPresent() ou orElse().

❌ 22. Factories génériques mal typées

Symptôme : Mauvais objet retourné.

Cause : Class<?> mal gérée.

Solution : Utiliser Class<T> paramétré.

📦 Catégorie 5 : Données & Builders

❌ 23. Hardcode des données

Symptôme : Tests non portables.

Cause : Valeurs fixes dans code.

Solution : Factories + externalisation.

❌ 24. Builder mutable réutilisé

Symptôme : Données incohérentes.

Cause : Réutilisation builder.

Solution : Builder immuable.

❌ 25. Mauvais Random

Symptôme : Collisions ID.

Cause : new Random() sans seed.

Solution : SecureRandom ou UUID.

❌ 26. JSON mal sérialisé

Symptôme : Tests API échouent.

Cause : ObjectMapper sans config.

Solution : Mapper global configuré.

❌ 27. Données non nettoyées

Symptôme : Tests pollués.

Cause : Data persistante.

Solution : Cleanup après test.

🌐 Catégorie 6 : APIs & IO

❌ 28. FileReader sans fermeture

Symptôme : File locks.

Cause : Pas de close().

Solution : try-with-resources.

❌ 29. Mauvais encodage

Symptôme : Caractères cassés.

Cause : Charset par défaut.

Solution : UTF-8 explicite.

❌ 30. HTTP client non fermé

Symptôme : Connexions épuisées.

Cause : HttpClient non fermé.

Solution : try-with-resources.

❌ 31. API calls synchrones lents

Symptôme : Tests > 10 min.

Cause : Pas d’async.

Solution : CompletableFuture.

❌ 32. Temps système utilisé pour ID

Symptôme : Collisions sur build rapide.

Cause : System.currentTimeMillis().

Solution : UUID.randomUUID().

🚀 Catégorie 7 : Performance & Mémoire

❌ 33. Objets lourds en static

Symptôme : Heap saturée.

Cause : Driver en static.

Solution : Scope contrôlé.

❌ 34. Pas de pool de drivers

Symptôme : Lancement lent.

Cause : Nouveau driver à chaque test.

Solution : Reuse ou pool.

❌ 35. String concatenation dans boucle

Symptôme : Perf dégradée.

Cause : new String à répétition.

Solution : StringBuilder.

❌ 36. Pas de GC tuning

Symptôme : Pauses aléatoires.

Cause : GC défaut.

Solution : G1GC pour gros projets.

❌ 37. Logs trop verbeux

Symptôme : Builds ralentis.

Cause : Log.debug massif.

Solution : Niveaux adaptés.

🧪 Catégorie 8 : Tests & Framework Integration

❌ 38. @BeforeClass mal utilisé

Symptôme : Setup partagé instable.

Cause : Driver unique.

Solution : @BeforeMethod isolé.

❌ 39. Mauvaise priorité TestNG

Symptôme : Ordre imprévisible.

Cause : priority mal géré.

Solution : Dépendances explicites.

❌ 40. Ignorer groups

Symptôme : Suite trop longue.

Cause : Pas de tag groups.

Solution : @Test(groups="smoke").

❌ 41. Annotations mélangées

Symptôme : JUnit/TestNG mélangés.

Cause : Migration incomplète.

Solution : Choisir un framework.

❌ 42. Assertions trop vagues

Symptôme : "expected true".

Cause : assertTrue(flag).

Solution : assertEquals avec message.

🧩 Catégorie 9 : Patterns avancés appliqués QA

❌ 43. Singleton anti-pattern

Symptôme : Tests bloqués.

Cause : WebDriver en singleton.

Solution : Factory + scope test.

❌ 44. Proxy mal implémenté

Symptôme : Logs vides.

Cause : InvocationHandler mal codé.

Solution : Reflection + Proxy bien géré.

❌ 45. Adapter incomplet

Symptôme : Méthodes non couvertes.

Cause : API partiellement mappée.

Solution : Implémentation complète.

❌ 46. Observer mal nettoyé

Symptôme : Memory leaks.

Cause : Listeners persistants.

Solution : removeObserver().

❌ 47. Factory sans cache

Symptôme : Objets recréés inutilement.

Cause : Factory naive.

Solution : Cache léger.

❌ 48. Strategy figée

Symptôme : Pas de flexibilité.

Cause : Implémentation unique.

Solution : Interface + plusieurs stratégies.

❌ 49. Decorator trop verbeux

Symptôme : Classes doublées.

Cause : Mauvais découpage.

Solution : Décorer uniquement besoin.

❌ 50. Command mal utilisée

Symptôme : Steps confus.

Cause : Pattern appliqué à tort.

Solution : Commands métier claires.

❌ 51. Proxy dynamique fragile

Symptôme : Tests cassés au refactor.

Cause : InvocationHandler dépendant noms méthodes.

Solution : API stable + annotations.

❌ 52. Abus de Reflection

Symptôme : Maintenance impossible.

Cause : Invocation privée partout.

Solution : Utiliser Reflection seulement en dernier recours.

Conclusion : Ces 52 erreurs condensent les pièges les plus fréquents rencontrés lors de l’usage de Java avancé pour construire des frameworks QA. Les éviter, c’est garantir robustesse, lisibilité et évolutivité.