QA How-To
Getting Started with Selenium WebDriver
Start browser automation with Selenium WebDriver using Java, durable locators, explicit waits, page objects, and practical guidance for reliable first tests.
1,864 words | Article schema | FAQ schema | Breadcrumb schema
Overview
Selenium WebDriver remains a foundational browser automation standard because it supports major browsers, multiple programming languages, and a vast ecosystem of grids and test runners. Its API sends commands to the browser through a driver implementation, giving tests control over navigation, elements, windows, cookies, and user interactions. That flexibility is powerful, but Selenium deliberately leaves framework choices such as assertions, lifecycle, reports, and dependency injection to you.
This tutorial uses Java, Maven, JUnit 5, and Selenium WebDriver to build a first test that is more robust than a recorded click script. You will configure dependencies, understand driver sessions, select elements, wait for dynamic behavior, manage cleanup, and introduce a focused page object. The goal is not merely to open a browser. It is to establish habits that still work when your suite grows from one test to hundreds.
Prepare Java and a Maven Project
Install a supported Java development kit and confirm it with `java -version` and `javac -version`. Create a Maven project with `src/test/java` for test code. In `pom.xml`, add Selenium Java and JUnit Jupiter test dependencies using current compatible versions, then configure the Maven Surefire plugin for JUnit 5. Running `mvn test` should complete even before a browser test exists. This small check separates Java or Maven setup problems from WebDriver problems.
A minimal dependency entry looks like `<dependency><groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId><version>VERSION</version><scope>test</scope></dependency>`. Add `org.junit.jupiter:junit-jupiter` in the same way. Keep versions in Maven properties so upgrades happen in one place. Do not copy an old tutorial's dependency numbers blindly. Selenium and browser release cycles move regularly, so verify approved versions in your team and let the Maven lock and CI process provide reproducibility. Document the supported Java version too.
- Use a JDK, not only a Java runtime, because Maven compiles test code.
- Keep test libraries in test scope.
- Run a plain JUnit test before adding browser setup.
- Commit the Maven wrapper if the team wants the same Maven version everywhere.
Start and Close a WebDriver Session
Modern Selenium includes Selenium Manager, which can discover, download, and cache an appropriate driver when you create `new ChromeDriver()`. A basic JUnit lifecycle is `private WebDriver driver; @BeforeEach void setUp() { driver = new ChromeDriver(); } @AfterEach void tearDown() { if (driver != null) driver.quit(); }`. `quit()` closes every window and ends the driver service. Calling only `close()` closes the current window and can leave processes behind.
A WebDriver object represents one browser session. Each test should normally receive a fresh session unless the team has carefully designed another isolation strategy. Sharing a static driver across parallel tests causes focus, cookie, and navigation collisions. For CI, browser options can enable headless mode, set a consistent viewport, and pass environment-specific flags. Keep those options in a small driver factory rather than sprinkling `ChromeOptions` through every test class.
Automate a User Journey and Assert the Result
Create a JUnit test such as `@Test void validUserCanSignIn() { driver.get(baseUrl + "/login"); driver.findElement(By.id("email")).sendKeys("learner@example.test"); driver.findElement(By.id("password")).sendKeys("CorrectPassword1!"); driver.findElement(By.cssSelector("button[type='submit']")).click(); assertEquals("Dashboard", driver.findElement(By.tagName("h1")).getText()); }`. This demonstrates navigation, element lookup, typing, clicking, and an assertion. Read the sequence as a user story: open login, provide credentials, submit, and verify the destination. Keeping that intent visible makes later refactoring safer and exposes steps that do not contribute to the behavior.
It also contains a timing weakness: the heading lookup may run before the dashboard renders. A reliable automation test must synchronize with application behavior, so the next step is not adding `Thread.sleep`. It is waiting for the result that proves the transition finished. Also notice that credentials are illustrative test data. Real test accounts and secrets should come from controlled test fixtures or environment variables, never from source code committed to a repository.
Use Locators as a Stability Contract
Selenium supports `By.id`, `By.name`, `By.cssSelector`, `By.xpath`, `By.linkText`, and other strategies. Prefer a unique stable ID, name, or dedicated testing attribute that expresses identity. CSS is useful when a single attribute is enough, such as `[data-testid='save-profile']`. A locator like `div.container > div:nth-child(2) > button` encodes layout and breaks when a designer adds a wrapper. Long XPath expressions have the same maintenance risk.
Treat locator quality as a conversation with developers. Accessible labels and test IDs are cheap to add before release and expensive to reverse engineer afterward. When a list contains repeated cards, first find the card that contains the required product name, then search within that card for its button. Scoped lookup avoids selecting the first Add button on the entire page. Do not cache `WebElement` instances across a re-render, because their DOM references can become stale. Locate at the moment of interaction or let a page method do so.
- Prefer unique IDs, names, and stable test attributes.
- Use concise CSS selectors when an element has a durable attribute.
- Scope child searches to a meaningful container for repeated content.
- Avoid index-based XPath unless order is the behavior under test.
- Re-find elements after navigation or dynamic replacement.
Replace Thread.sleep With Explicit Waits
Dynamic pages rarely become ready at a fixed time. `Thread.sleep(3000)` always costs three seconds and still fails if CI needs longer. An explicit wait polls for a condition and continues as soon as it succeeds: `WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement heading = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("h1"))); assertEquals("Dashboard", heading.getText());`. The timeout is a maximum, not a mandatory delay. Successful conditions continue immediately.
Choose a condition that represents readiness: an element is visible, a button is clickable, a URL contains the expected route, text appears, or an overlay disappears. Presence only means an element exists in the DOM, not that a user can see or click it. Avoid mixing a large implicit wait with explicit waits, because nested polling can create confusing delays. Many mature Selenium suites set no implicit wait and provide reusable explicit wait helpers whose failure messages explain what business state was expected.
Handle Common Browser Interactions Correctly
Standard form elements have purpose-built support. Wrap a real `<select>` element with `Select` and choose by visible text or value. Use `driver.switchTo().alert()` for JavaScript alerts, and switch into an iframe before locating elements inside it, then return with `defaultContent()`. For a new tab, store the original window handle, trigger the opening action, wait until the handle count grows, switch to the new handle, and assert its content. Never assume the second item in a handle set is always the new tab.
For mouse hover, drag, or keyboard chords, build an `Actions` sequence. JavaScript execution is an escape hatch, not the first response when a normal click fails. A failed click often exposes a real overlay, disabled control, or synchronization defect that a user would also encounter. If uploading a file, send an absolute test-fixture path to an `<input type='file'>` rather than driving the operating system file picker, which WebDriver does not control consistently.
Introduce a Small Page Object
A page object gives a screen a stable testing interface. For example, `class LoginPage { private final WebDriver driver; private final By email = By.id("email"); private final By password = By.id("password"); private final By submit = By.cssSelector("button[type='submit']"); LoginPage(WebDriver driver) { this.driver = driver; } void signIn(String user, String pass) { driver.findElement(email).sendKeys(user); driver.findElement(password).sendKeys(pass); driver.findElement(submit).click(); } }`. The test can now express intent with `loginPage.signIn(user, pass)`.
Keep the object focused on one page or reusable component. It should own locators and meaningful interactions, not the entire application and every assertion. Returning a new page object after navigation can make transitions explicit. Add a wait inside a method when readiness is intrinsic to that operation, such as waiting for the login form before typing. Avoid giant base pages full of unrelated generic methods. They create inheritance coupling and make simple actions harder to trace.
Diagnose Failures With Evidence
A `NoSuchElementException` usually means the locator is wrong, the element is in another frame or window, or the lookup happened too early. A `StaleElementReferenceException` means the DOM node represented by a stored `WebElement` was replaced. An `ElementClickInterceptedException` means another element received the click. Each exception suggests a different investigation, so replacing all three with a longer sleep only delays the failure.
On failure, capture a screenshot, current URL, page source when appropriate, browser console logs, and test data identifiers. JUnit extensions can centralize this evidence. Reproduce with the same browser, version, viewport, and environment as CI. Check server and network logs alongside UI evidence when the page depends on APIs. A test report should answer where the failure occurred and preserve enough state for diagnosis without rerunning locally and hoping to see it.
Scale From One Test to a Responsible Suite
Organize packages by product capability, keep test data builders separate from page interactions, and use tags to define smoke and regression groups. Parallel execution requires a driver per thread and independent accounts or records. Selenium Grid or a cloud provider can distribute sessions across browser and operating system combinations, but distribution amplifies existing flakiness. Stabilize tests locally before buying more concurrency.
Run a small, high-value browser set on every change and broader compatibility coverage on a schedule that matches risk. Keep most validation below the UI at unit, service, or API level, where it runs faster and diagnoses failures more precisely. UI automation should protect critical journeys and integration points. Measure pass rate, duration, and recurring failure causes. The long-term quality of a Selenium framework is visible in how quickly engineers trust and understand a failure, not in its raw test count.
Frequently Asked Questions
Is Selenium WebDriver still worth learning?
Yes. Selenium remains widely used in established automation stacks and supports many languages, browsers, grids, and vendors. Learning it also teaches synchronization, element identity, browser sessions, and test architecture concepts that transfer to other tools.
Which language is best for learning Selenium?
Use the language your target team or market uses. Java has a large Selenium ecosystem, while Python, C#, and JavaScript are also valid. Framework quality depends more on engineering practices than on the binding.
Do I need to download ChromeDriver manually?
Often no. Current Selenium releases include Selenium Manager, which can resolve and cache suitable drivers. Locked-down networks and enterprise environments may still require a preapproved driver path or an internal artifact mirror.
What is the difference between implicit and explicit waits?
An implicit wait affects element searches globally, while an explicit wait polls for a specific condition at a specific point. Explicit waits communicate the expected state more clearly. Mixing large implicit and explicit waits can produce surprising total delays.
Why do Selenium elements become stale?
A stored WebElement points to a particular DOM node. If navigation or a framework re-render replaces that node, the reference is stale even when a similar element appears on screen. Locate it again after the update.
Should every Selenium test use the Page Object Model?
Not necessarily. Page objects help when locators and workflows repeat, but tiny suites can start with clear test methods. Introduce objects at stable screen or component boundaries and avoid building a complicated hierarchy before patterns emerge.