Migrate from Selenium + TestNG
If you already run a hand-rolled Selenium + TestNG framework, you have written — and now maintain — a driver factory, a waits utility, a retry analyzer, screenshot-on-failure glue, and a reporting integration. Selenium Boot ships all of that as one dependency.
This guide is a side-by-side "your current setup → Selenium Boot equivalent." The short version: most of the plumbing you maintain today simply gets deleted.
Selenium Boot is still Selenium. WebDriver, By, WebElement, and your existing page-object patterns all still work — you're removing boilerplate, not switching tools.
Setup — swap dependencies
Remove your Selenium, WebDriverManager, and reporting dependencies and add one:
<dependency>
<groupId>io.github.seleniumboot</groupId>
<artifactId>selenium-boot</artifactId>
<version>3.1.1</version>
</dependency>
Selenium Boot brings Selenium (and TestNG) transitively. You no longer declare selenium-java, webdrivermanager, or a reporting library yourself.
Then create a small selenium-boot.yml — see config mapping below.
1. Driver setup
Before — a driver factory, ThreadLocal juggling for parallel runs, and WebDriverManager to fetch binaries:
public class DriverFactory {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
public static void createDriver() {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--start-maximized");
DRIVER.set(new ChromeDriver(options));
DRIVER.get().manage().timeouts()
.implicitlyWait(Duration.ofSeconds(10));
}
public static WebDriver getDriver() { return DRIVER.get(); }
public static void quitDriver() {
DRIVER.get().quit();
DRIVER.remove();
}
}
public class BaseTest {
@BeforeMethod public void setUp() { DriverFactory.createDriver(); }
@AfterMethod public void tearDown() { DriverFactory.quitDriver(); }
}
After — extend BaseTest. Driver creation, per-thread isolation, and teardown are handled for you:
import com.seleniumboot.test.BaseTest;
import org.testng.annotations.Test;
public class LoginTest extends BaseTest {
@Test
public void loginTest() {
open(); // navigates to execution.baseUrl
// ...
}
}
- No
WebDriverManager. Modern Selenium (4.6+) bundles Selenium Manager, which downloads the right driver binary automatically. Selenium Boot uses it — delete the.setup()calls and the dependency. See Migrate from WebDriverManager for the details. - No
ThreadLocal.DriverManagerisolates the driver per thread, so parallel runs are safe out of the box. - Need the raw driver? It's still there:
getDriver().
Delete implicitlyWait(...). Selenium Boot's locators auto-wait explicitly; mixing implicit and explicit waits is a classic source of flaky, slow tests.
2. Waits
Before — a WaitUtils helper wrapping WebDriverWait / ExpectedConditions, imported into every page:
public class WaitUtils {
public static WebElement waitVisible(WebDriver driver, By locator) {
return new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
public static void waitClickable(WebDriver driver, By locator) {
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.elementToBeClickable(locator));
}
}
// usage
WaitUtils.waitVisible(driver, By.id("login")).click();
After — every locator auto-waits, and WaitEngine (pre-configured from your timeouts.explicit) covers the explicit cases:
$("#login").click(); // auto-waits for clickable
getWait().waitForInvisible(By.cssSelector(".spinner"));
getWait().waitForText(By.cssSelector("h1"), "Welcome back");
No Thread.sleep(), no per-page WebDriverWait construction, no passing driver around. See the WaitEngine guide.
3. Retry / flaky tests
Before — an IRetryAnalyzer plus a listener to attach it to every method:
public class RetryAnalyzer implements IRetryAnalyzer {
private int count = 0;
private static final int MAX = 2;
@Override public boolean retry(ITestResult result) {
return count++ < MAX;
}
}
public class RetryListener implements IAnnotationTransformer {
@Override public void transform(ITestAnnotation ann, Class c,
Constructor ctor, Method m) {
ann.setRetryAnalyzer(RetryAnalyzer.class);
}
}
// + register the listener in testng.xml
After — one config line turns on retry for the whole suite:
retry:
enabled: true
maxAttempts: 2 # total attempts including the first run
Override per test with @Retryable when you need to:
@Test
@Retryable(maxAttempts = 3)
public void flakyTest() { /* ... */ }
Recovered vs. still-failing retries are broken out in the report. See the Retry guide.
4. Screenshots on failure
Before — an ITestListener that reaches into the driver on onTestFailure, encodes a PNG, and writes it somewhere your report can find:
public class ScreenshotListener implements ITestListener {
@Override public void onTestFailure(ITestResult result) {
WebDriver driver = DriverFactory.getDriver();
File png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
// ...copy to /screenshots, attach to report, handle IOException...
}
}
After — nothing. Selenium Boot captures a screenshot on every failure automatically and embeds it in the HTML report. Delete the listener. See Screenshots.
5. Reporting
Before — wire in ExtentReports/Allure: a listener, a flush() in an @AfterSuite, and per-test logging calls scattered through your code.
After — a self-contained HTML report at target/selenium-boot-report.html (pass-rate gauge, retries, embedded screenshots, flakiness) and a JUnit XML file for CI, both generated automatically after every run. Add named steps with the optional Step Logging API if you want richer reports.
See HTML Report and JUnit XML.
What gets deleted
| Your current setup | Selenium Boot |
|---|---|
DriverFactory + ThreadLocal<WebDriver> | ✅ Built in — extend BaseTest |
WebDriverManager.chromedriver().setup() | ✅ Selenium Manager (automatic) |
| Implicit-wait config | ✅ Auto-waiting locators |
WaitUtils / WebDriverWait helpers | ✅ WaitEngine + auto-wait |
IRetryAnalyzer + IAnnotationTransformer | ✅ retry: config + @Retryable |
Screenshot-on-failure ITestListener | ✅ Automatic on failure |
| ExtentReports/Allure wiring | ✅ HTML report + JUnit XML |
@BeforeMethod / @AfterMethod lifecycle glue | ✅ Framework-managed lifecycle |
Your page objects and @Test methods stay — they just get shorter.
Config mapping
Settings that lived in testng.xml attributes and scattered constants move into one file:
execution:
baseUrl: https://your-app.com
parallel: methods # was: <suite parallel="methods">
threadCount: 4 # was: thread-count="4"
browser:
name: chrome
headless: false # auto-forced true when CI is detected
timeouts:
explicit: 10 # was: your WaitUtils constant
pageLoad: 30
retry:
enabled: true
maxAttempts: 2 # was: RetryAnalyzer MAX
You still keep a minimal testng.xml to list your test classes — Selenium Boot registers its own listeners, so you can remove the <listeners> block. See the Configuration Reference for every option.
Migrating incrementally
You don't have to convert everything at once:
- Add the dependency and a
selenium-boot.yml. - Point one test class at
BaseTest, delete its@BeforeMethod/@AfterMethod, and run it. - Once green, delete your
DriverFactory,WaitUtils, retry analyzer, and screenshot listener as the last class stops referencing them.
Because Selenium Boot is Selenium, a half-migrated suite runs fine.
Next steps
- Getting Started — the 5-minute version
- BaseTest / BasePage — the base classes you'll extend
- Accessibility-First Locators —
getByRole/getByLabel, once the boilerplate is gone - Configuration Reference — the full
selenium-boot.yml