Module 2: Selenium WebDriver Basics

Test Automation · Module 2

Selenium WebDriver Basics

This is where automation stops being theory and starts driving a real browser. By the end of this module you will have a working Java project that opens Chrome on its own, navigates to a page, reads information from it, types into fields, clicks buttons, and shuts down cleanly—all from code you wrote. We will build it up piece by piece, explain why each line exists, and finish with a complete, runnable test class you can copy and extend. The goal is not just to make it work, but to understand what is happening underneath so that when something breaks later, you know where to look.

Learning objectives

By the end of this lesson you will be able to:

  • Explain what Selenium WebDriver is and how it drives a real browser through the W3C WebDriver protocol.
  • Describe the driver architecture—your code, the driver, and the browser—and where each sits.
  • Set up a Java + Maven project with selenium-java 4.x and TestNG from a complete pom.xml.
  • Explain how Selenium Manager auto-resolves browser drivers so you never download chromedriver by hand.
  • Launch a browser and use get, getTitle, getCurrentUrl, and findElement.
  • Perform the core interactions: sendKeys, click, getText, clear, and submit.
  • Write a complete first test and explain why driver.quit() matters—and how it differs from close().

What Selenium WebDriver actually is

Selenium WebDriver is a library that lets your code control a real web browser the same way a person would—clicking links, typing into boxes, pressing buttons, reading what is on screen. The crucial word is real: WebDriver does not simulate a fake browser or parse HTML in a sandbox. It drives an actual Chrome, Firefox, or Edge window, so your test sees exactly what a user would see, including the JavaScript, the styling, and the real rendering.

It does this through a published standard called the W3C WebDriver protocol. This matters: “WebDriver” is not just a Selenium feature, it is an official web specification that browser makers agreed to support. Because of that standard, the same commands—“navigate to this URL”, “find this element”, “click it”—work across every compliant browser, and the skills you build here transfer to other WebDriver-based tools like Appium for mobile.

The architecture: three players, one conversation

When your test runs, three separate programs are involved. Picture it as a chain of command. Your test code (the Java program) issues a high-level instruction like driver.click(). The Selenium library turns that into an HTTP request and sends it to the browser driver—a small executable such as chromedriver that acts as a translator. The driver speaks the browser’s native automation language and tells the actual browser to perform the action. The browser does it, reports back up the chain, and your code receives the result. So it is: your code → the driver (translator) → the real browser, and back again. Every single Selenium command is one round-trip through that chain over HTTP—which, as a bonus, is also why network speed and page readiness affect your tests, a theme we return to constantly.

Project setup: Java + Maven

We manage the project with Maven, the standard Java build and dependency tool. Instead of hunting down Jar files, you declare what you need in a single file called pom.xml and Maven downloads it for you. Here is a complete, working pom.xml for a Selenium 4.x + TestNG project. Create a new Maven project and replace its pom.xml with this:

<?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.qalaunch</groupId>
  <artifactId>selenium-basics</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!-- Selenium WebDriver 4.x -->
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>4.25.0</version>
    </dependency>

    <!-- TestNG test framework -->
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.10.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Two dependencies, that is all you need to begin: selenium-java pulls in everything to drive browsers, and testng gives you the test framework (the @Test annotations, assertions, and lifecycle hooks we use below and throughout the rest of the course). After saving, let your IDE import the Maven changes, or run mvn clean compile from the terminal to download everything.

Tutor tip: Selenium Manager means no more chromedriver downloads

If you learned Selenium a few years ago, you remember the pain: download the exact chromedriver version matching your Chrome, put it somewhere on your PATH, and re-download it every time Chrome auto-updated. Those days are over. Since version 4.6, Selenium ships with Selenium Manager, which automatically detects your installed browser, downloads the matching driver, caches it, and wires it up—all the first time you run new ChromeDriver(). You do not call it, configure it, or even see it. As long as you have Chrome (or Firefox/Edge) installed, the pom.xml above is genuinely all the setup you need. No manual driver management appears anywhere in this course.

Launching a browser and navigating

With the project set up, launching a browser is a single line. The WebDriver interface is the steering wheel for the whole session; ChromeDriver is the concrete implementation that drives Chrome.

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

WebDriver driver = new ChromeDriver();   // Selenium Manager resolves the driver, Chrome opens
driver.get("https://the-internet.herokuapp.com/login");

Notice we type the variable as WebDriver (the interface), not ChromeDriver (the class). This is good practice: your test code is written against the general interface, so switching to Firefox later is a one-line change (new FirefoxDriver()) and nothing else in your test needs to move. driver.get(url) navigates to a page and—importantly—waits for the page’s initial load to finish before returning.

Asking the browser about itself

Once you are on a page, a few read-only methods tell you where you are. These are the backbone of your first assertions.

Method What it returns Typical use
driver.getTitle() The page’s <title> text, as a String. Confirm you landed on the right page.
driver.getCurrentUrl() The current URL in the address bar. Confirm a redirect or navigation happened.
driver.getPageSource() The full HTML of the page. Debugging; occasional content checks.
driver.get("https://the-internet.herokuapp.com/login");
System.out.println(driver.getTitle());        // "The Internet"
System.out.println(driver.getCurrentUrl());   // "https://the-internet.herokuapp.com/login"

Finding elements

To interact with anything on a page—a text box, a button, a heading—you must first locate it. The method is driver.findElement(By...). It takes a locator (a By strategy describing how to find the element) and returns a WebElement, which is your handle on that element.

import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

WebElement username = driver.findElement(By.id("username"));
WebElement heading  = driver.findElement(By.tagName("h2"));
WebElement loginBtn = driver.findElement(By.cssSelector("button[type='submit']"));

There are several By strategies—By.id, By.name, By.className, By.cssSelector, By.xpath, By.linkText, and more. Choosing good, stable locators is so important that the whole of Module 3 is devoted to it. For now, know that By.id is the most reliable when an element has one, because IDs are meant to be unique.

findElement throws when nothing is found

A very common beginner surprise: findElement does not return null when the element is missing—it throws a NoSuchElementException and your test fails on that line. Beginners often write if (driver.findElement(...) != null) expecting a null check to work; it never will, because the exception fires first. Even more common: the element genuinely exists but your script looked for it a split second before the page finished rendering, so you get NoSuchElementException on an element you can clearly see in the browser. That is the timing/flakiness problem from Module 1, and the proper fix is explicit waits—covered next module. For now, just recognise the exception: it means “I could not find that element right now.”

Core interactions

Once you have a WebElement, these are the actions you will use constantly. They are the verbs of browser automation.

Action Code What it does
Type text element.sendKeys("tomsmith") Types into an input, as if from the keyboard. Appends—does not replace.
Clear a field element.clear() Empties an input before typing fresh text.
Click element.click() Clicks a button, link, checkbox, etc.
Read text element.getText() Returns the visible text inside the element.
Submit a form element.submit() Submits the form the element belongs to.
Read a value element.getDomProperty("value") Returns a live DOM property, e.g. an input’s current text. (Use getDomAttribute for the value written in the HTML markup.)

A couple of these deserve a closer look. sendKeys appends to whatever is already in the field, so when re-entering a value, call clear() first or you will end up with the old and new text concatenated. And submit() is a convenient shortcut: call it on any element inside a <form> and Selenium submits the whole form, which is handy when there is no obvious submit button to click—though clicking the actual button is usually clearer and closer to what a real user does.

WebElement username = driver.findElement(By.id("username"));
username.clear();                 // ensure the field is empty
username.sendKeys("tomsmith");    // type the username

WebElement password = driver.findElement(By.id("password"));
password.sendKeys("SuperSecretPassword!");

driver.findElement(By.cssSelector("button[type='submit']")).click();   // submit the form

String flash = driver.findElement(By.id("flash")).getText();
System.out.println(flash);        // read the success/error banner

Your first complete, runnable test

Now we assemble everything into a real TestNG test class. This logs into a public practice site, verifies the success message, and cleans up afterwards. It uses TestNG’s lifecycle annotations—@BeforeMethod runs before each test to open the browser, @AfterMethod runs after to close it—which is the structure every serious Selenium suite uses.

package com.qalaunch;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class LoginTest {

    private WebDriver driver;

    @BeforeMethod
    public void setUp() {
        driver = new ChromeDriver();   // Selenium Manager resolves the driver automatically
    }

    @Test
    public void validLoginShowsSuccessMessage() {
        driver.get("https://the-internet.herokuapp.com/login");

        // sanity-check we are on the right page
        Assert.assertEquals(driver.getTitle(), "The Internet");

        // fill in the form
        driver.findElement(By.id("username")).sendKeys("tomsmith");
        driver.findElement(By.id("password")).sendKeys("SuperSecretPassword!");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        // verify the outcome
        String banner = driver.findElement(By.id("flash")).getText();
        Assert.assertTrue(banner.contains("You logged into a secure area!"),
                "Expected success banner but got: " + banner);

        // and that the URL changed to the secure page
        Assert.assertTrue(driver.getCurrentUrl().contains("/secure"),
                "Expected to be redirected to the secure area.");
    }

    @AfterMethod
    public void tearDown() {
        if (driver != null) {
            driver.quit();   // always clean up the browser and driver
        }
    }
}

Run it with mvn test or by right-clicking the class in your IDE. Chrome opens, logs in, the assertions pass, and the browser closes. That is a complete, professional-shaped automated test—setup, action, assertion, teardown.

Going deeper: driver.quit() vs driver.close()—and why teardown is non-negotiable

These two methods look similar and trip up almost everyone at first:

driver.close()
Closes the current browser window/tab. If it is the last window, the browser also exits—but the underlying driver process may linger.
driver.quit()
Ends the entire WebDriver session: it closes every window the driver opened and shuts down the background driver process, freeing all resources.

Almost always you want quit(). The reason is resource leaks. Every new ChromeDriver() spawns a browser process and a chromedriver process. If a test ends without quit(), those processes stay alive—“zombie” browsers. Run a few hundred tests without cleanup and your machine (or your CI build agent) crawls to a halt under hundreds of orphaned Chrome processes eating memory.

The second lesson is where to put it. Calling quit() as the last line of your test is a trap: if an assertion fails or an exception is thrown earlier, that line never runs and the browser leaks. The fix is to clean up in a place that always executes—either a try/finally block (the cleanup goes in finally, which runs whether the test passes or throws) or, far better, a framework teardown hook like TestNG’s @AfterMethod as shown above. TestNG guarantees @AfterMethod runs after every test regardless of pass, fail, or exception, so the browser is always cleaned up. That is why real suites put quit() in teardown, never inline.

Hands-on exercise

Create the Maven project from the pom.xml above, then write a TestNG class that exercises everything in this module against https://the-internet.herokuapp.com:

  1. In @BeforeMethod, launch Chrome; in @AfterMethod, call driver.quit().
  2. Navigate to the /login page and assert that getTitle() equals "The Internet".
  3. Print getCurrentUrl() before and after logging in, and observe how it changes to include /secure.
  4. Log in with tomsmith / SuperSecretPassword! and assert the success banner appears.
  5. Write a second test for the negative case: log in with a wrong password and assert the banner contains "Your password is invalid!".
  6. In the negative test, after the failed attempt, use clear() then sendKeys() to re-enter the correct password and confirm it then succeeds—proving you understand why clear() matters.

Knowledge check

  1. In one sentence, what does Selenium WebDriver let you do, and what standard is it built on?
  2. Name the three programs involved when a Selenium command runs.
  3. What does Selenium Manager do, and what manual task does it remove?
  4. Which method returns the page title, and which returns the current URL?
  5. What happens when findElement cannot locate an element?
  6. Why should you call clear() before sendKeys() when re-entering a value?
  7. Explain the difference between driver.quit() and driver.close(), and why teardown belongs in @AfterMethod rather than the last line of the test.
Show answers
  1. It lets your code drive a real browser (click, type, navigate, read) as a user would, built on the W3C WebDriver protocol, an official web standard.
  2. Your test code, the browser driver (e.g. chromedriver, the translator), and the actual browser.
  3. It automatically detects your browser, downloads the matching driver, caches it, and wires it up—removing all manual chromedriver downloading and PATH setup.
  4. driver.getTitle() returns the title; driver.getCurrentUrl() returns the current URL.
  5. It throws a NoSuchElementException (it does not return null). Often the cause is timing—the element was not ready yet—fixed by explicit waits.
  6. Because sendKeys appends to the existing value; without clear() the old and new text would be joined together.
  7. quit() ends the whole session and shuts down the driver process (no leaks); close() only closes the current window. Teardown goes in @AfterMethod because it always runs—even when a test fails or throws—so the browser is never left as a zombie process.

Key takeaways

  • Selenium WebDriver drives a real browser via the W3C WebDriver protocol: your code → driver → browser, over HTTP.
  • Setup is just two Maven dependencies—selenium-java and testng—and Selenium Manager handles drivers automatically, so no manual chromedriver downloads.
  • get, getTitle, getCurrentUrl, and findElement are your navigation and inspection basics; findElement throws when nothing is found.
  • Core interactions: sendKeys, clear, click, getText, submit—remember sendKeys appends.
  • Always end a session with driver.quit() (not just close()), placed in try/finally or a TestNG @AfterMethod so it always runs and never leaks browser processes.

Go deeper

The bare new ChromeDriver() is fine for learning, but real suites almost always pass a ChromeOptions object to control how the browser launches. The two settings you will reach for first are headless mode (no visible window — essential on a CI server that has no screen) and a fixed window size, so your tests are not at the mercy of whatever resolution the build agent happens to have:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");        // modern headless, Chrome 109+
options.addArguments("--window-size=1920,1080");
options.addArguments("--disable-gpu");

WebDriver driver = new ChromeDriver(options);

getText() only returns visible text. A common trap is calling getText() on an element that is in the DOM but hidden (display:none) — you get an empty string, not the content. When you need the text regardless of visibility, read the DOM property instead with element.getDomProperty("textContent").

Attribute versus property is a genuine distinction in Selenium 4. getDomAttribute("value") returns what was written in the HTML markup; getDomProperty("value") returns the live current value after the user has typed. For an input the user has edited, only the property reflects reality — a classic source of “why is my assertion reading the old value?” confusion.

  • For hovers, drag-and-drop, right-clicks and key chords that a plain click() cannot express, use the Actions class: new Actions(driver).moveToElement(menu).click(submenuItem).perform();
  • Capture a screenshot on failure for debugging by casting the driver: ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE), then copy the file somewhere your report can display it.

Save to list

Scroll to Top