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.
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.
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 |
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
});
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.
- 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
- ❌ Utiliser
<version>LATEST</version>(instable) - ❌ Ne pas fixer les versions (conflits de dépendances)
- ❌ Mélanger Selenium 3 et 4 (incompatibilité)
- ✅ Toujours fixer les versions exactes
- ✅ Centraliser dans
<properties> - ✅ Vérifier la compatibilité Java (min. Java 11 pour Selenium 4)
- ✅ Utiliser
mvn dependency:treepour 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 |
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
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) |
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.
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
- ✅ 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
}
}
- 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>