Why build a custom framework? Pourquoi construire un framework sur mesure ?
Using Selenium directly without structure quickly becomes unmaintainable. Tests duplicate selectors, share mutable state, and break as soon as the UI changes. A proper framework solves these problems by enforcing clear separation between test logic, page interactions, and test data. Utiliser Selenium directement sans structure devient vite ingérable. Les tests dupliquent les sélecteurs, partagent un état mutable, et cassent dès que l'UI change. Un framework solide règle ces problèmes en imposant une séparation claire entre la logique de test, les interactions avec les pages et les données de test.
This article walks through the architecture I put together: a Java + Selenium + TestNG framework built around the Page Object Model, with reusable utilities, external test data, and full driver isolation between tests. Cet article présente l'architecture que j'ai mise en place : un framework Java + Selenium + TestNG construit autour du Page Object Model, avec des utilitaires réutilisables, des données de test externes et une isolation complète du driver entre les tests.
Project structure Structure du projet
The project follows a standard Maven layout. Each layer has a single responsibility — pages know the UI, tests know the scenarios, utilities know the infrastructure. Le projet suit une structure Maven standard. Chaque couche a une responsabilité unique — les pages connaissent l'UI, les tests connaissent les scénarios, les utilitaires connaissent l'infrastructure.
Page Object Model Page Object Model
Every page of the application gets its own class. Selectors are declared once as private fields. Public methods represent what a user can do on that page — never raw Selenium calls inside test classes. Chaque page de l'application a sa propre classe. Les sélecteurs sont déclarés une seule fois en champs privés. Les méthodes publiques représentent ce qu'un utilisateur peut faire sur cette page — jamais d'appels Selenium bruts dans les classes de test.
public class LoginPage extends BasePage { // Selectors — defined once, maintained in one place private final By usernameField = By.id("username"); private final By passwordField = By.id("password"); private final By loginButton = By.cssSelector("button[type='submit']"); private final By errorMessage = By.id("error-msg"); public LoginPage(WebDriver driver) { super(driver); } // Actions — what a user does on this page public DashboardPage loginAs(String user, String pass) { type(usernameField, user); type(passwordField, pass); click(loginButton); return new DashboardPage(driver); } public String getErrorMessage() { return getText(errorMessage); } }
The BasePage holds shared wrappers
around Selenium — explicit waits, safe clicks, typed text input. Every page inherits these,
so there's no repeated boilerplate.
La BasePage contient les wrappers
partagés autour de Selenium — attentes explicites, clics sécurisés, saisie de texte typée.
Chaque page en hérite, donc pas de code répété.
Driver isolation with ThreadLocal Isolation du driver avec ThreadLocal
When tests run in parallel, a shared static WebDriver causes race conditions and
flaky failures. The solution: one driver per thread via
ThreadLocal<WebDriver>.
Each test gets its own browser instance — fully isolated, fully parallel-safe.
Quand les tests tournent en parallèle, un WebDriver statique partagé provoque des
race conditions et des échecs instables. La solution : un driver par thread
via ThreadLocal<WebDriver>.
Chaque test obtient sa propre instance de navigateur — totalement isolée, totalement sûre en parallèle.
public class DriverManager { private static final ThreadLocal<WebDriver> driverPool = new ThreadLocal<>(); public static void initDriver(String browser) { WebDriver driver = browser.equalsIgnoreCase("firefox") ? new FirefoxDriver() : new ChromeDriver(); driver.manage().window().maximize(); driverPool.set(driver); } public static WebDriver getDriver() { return driverPool.get(); } public static void quitDriver() { if (driverPool.get() != null) { driverPool.get().quit(); driverPool.remove(); // avoid memory leaks } } }
⚠️ Always call driverPool.remove()
after quitting. Without it, the ThreadLocal reference survives thread reuse in a pool
and causes memory leaks in long CI runs.
⚠️ Toujours appeler driverPool.remove()
après le quit. Sans ça, la référence ThreadLocal survit à la réutilisation des threads
dans un pool et provoque des fuites mémoire dans les longs runs CI.
Data-driven testing Tests data-driven
Test data lives in an Excel file outside the codebase. A TestNG
@DataProvider reads it at runtime
and feeds each row as a separate test execution — valid credentials, invalid passwords,
locked accounts, all covered without duplicating test logic.
Les données de test se trouvent dans un fichier Excel hors du code. Un
@DataProvider TestNG les lit au runtime
et passe chaque ligne comme exécution de test séparée — identifiants valides, mots de passe
invalides, comptes bloqués, tout couvert sans dupliquer la logique de test.
public class LoginTest extends BaseTest { @DataProvider(name = "loginData") public Object[][] loginData() { return ExcelReader.getData( "src/test/resources/data/login_data.xlsx", "LoginSheet" ); } @Test(dataProvider = "loginData") public void testLogin(String username, String password, String expectedResult) { LoginPage loginPage = new LoginPage(DriverManager.getDriver()); loginPage.open(config.getProperty("baseUrl")); if (expectedResult.equals("success")) { DashboardPage dashboard = loginPage.loginAs(username, password); Assert.assertTrue(dashboard.isLoaded()); } else { loginPage.loginAs(username, password); Assert.assertTrue(loginPage.getErrorMessage().contains("Invalid")); } } }
-
One test method, N scenariosUne méthode, N scénarios
Adding a new test case means adding a row in Excel — zero code change. Ajouter un cas de test signifie ajouter une ligne dans Excel — zéro changement de code. -
Consistent structureStructure cohérente
Each row contains: username, password, expectedResult. The DataProvider maps columns to parameters automatically. Chaque ligne contient : username, password, expectedResult. Le DataProvider mappe les colonnes aux paramètres automatiquement. -
Edge cases firstLes cas limites en premier
Empty fields, SQL injection attempts, XSS strings — all go in the Excel, not scattered across dozens of test methods. Champs vides, tentatives d'injection SQL, strings XSS — tout dans l'Excel, pas dispersé dans des dizaines de méthodes de test.
Reusable utilities Utilitaires réutilisables
Three utility classes handle the infrastructure so test classes stay clean: Trois classes utilitaires gèrent l'infrastructure pour que les classes de test restent propres :
-
ConfigReader
Loadsconfig.propertiesonce at startup. Base URL, browser type, timeouts, and environment flags are injected — no hardcoded values anywhere in tests. Chargeconfig.propertiesune fois au démarrage. URL de base, type de navigateur, timeouts et flags d'environnement sont injectés — aucune valeur codée en dur dans les tests. -
ExcelReader
Uses Apache POI to read any sheet as a 2D Object array. Handles merged cells, empty rows, and type conversion — the DataProvider receives clean, ready-to-use data. Utilise Apache POI pour lire n'importe quelle feuille en tableau 2D. Gère les cellules fusionnées, les lignes vides et la conversion de types — le DataProvider reçoit des données propres, prêtes à l'emploi. -
ScreenshotUtil
Captures a screenshot on every test failure via a TestNG listener. Screenshots are saved with the test name and timestamp — no manual call in test code. Capture une capture d'écran à chaque échec de test via un listener TestNG. Les screenshots sont sauvegardés avec le nom du test et l'horodatage — aucun appel manuel dans le code de test.
💡 Design rule : if the same three lines appear in two different tests, extract them into a utility or a BasePage method. Duplication is the first sign of a brittle framework. 💡 Règle de conception : si les mêmes trois lignes apparaissent dans deux tests différents, extrais-les dans un utilitaire ou une méthode de BasePage. La duplication est le premier signe d'un framework fragile.
Key takeaways Ce qu'il faut retenir
-
POM is non-negotiableLe POM n'est pas optionnel
One UI change should touch exactly one file. If you change a selector in five places, the structure is wrong. Un changement d'UI ne devrait toucher qu'un seul fichier. Si tu changes un sélecteur à cinq endroits, la structure est mauvaise. -
ThreadLocal for real parallelismThreadLocal pour un vrai parallélisme
Parallel Selenium without ThreadLocal is a recipe for flaky, unreliable tests. Always isolate the driver. Selenium en parallèle sans ThreadLocal est une recette pour des tests instables. Toujours isoler le driver. -
Externalize test dataExternaliser les données de test
Hardcoded test values in code are a maintenance trap. Excel, JSON or YAML — keep data out of the source. Les valeurs codées en dur dans le code sont un piège de maintenance. Excel, JSON ou YAML — garder les données hors du source. -
Let listeners do the cross-cutting workLaisser les listeners gérer le cross-cutting
Screenshots on failure, logging, retry logic — these belong in TestNG listeners, not scattered in test methods. Screenshots en échec, logging, retry — tout ça appartient aux listeners TestNG, pas éparpillé dans les méthodes de test.