The skeleton is in place. Time to write Java that drives a real browser. This lesson walks you from an empty tests/ package to a green test run that opens Chrome, navigates to a page, makes two assertions, and cleanly closes the browser. Every annotation, every method call, and the lifecycle that ties them together — explained line by line. You'll write maybe forty lines of code; what you'll learn is the shape of every Selenium test you'll ever write.
The complete first test
Create src/test/java/com/mycompany/tests/tests/HomePageTest.java:
package com.mycompany.tests.tests;
import io.github.bonigarcia.wdm.WebDriverManager;
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 HomePageTest {
WebDriver driver;
@BeforeMethod
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@Test
public void shouldDisplayWelcomeTitle() {
driver.get("https://qa.codes");
String title = driver.getTitle();
Assert.assertTrue(
title.contains("qa.codes"),
"Page title should contain 'qa.codes' but was: " + title
);
}
@Test
public void shouldHaveLearnLink() {
driver.get("https://qa.codes");
boolean learnLinkDisplayed = driver
.findElement(By.linkText("Learn"))
.isDisplayed();
Assert.assertTrue(learnLinkDisplayed, "Learn link should be visible on the home page");
}
@AfterMethod
public void teardown() {
if (driver != null) {
driver.quit();
}
}
}Right-click the file in IntelliJ → Run 'HomePageTest'. Chrome flashes open, navigates to qa.codes, runs the assertions, and closes. Twice — once per @Test method. The TestNG report at the bottom of IntelliJ shows two green ticks. That's it. You've automated a browser.
Anatomy — every keyword does work
Read the file from top to bottom. Each block has a job:
package com.mycompany.tests.tests; — matches the folder path under src/test/java. Java enforces the match at compile time.
import ... — Selenium classes (WebDriver, By, ChromeDriver), TestNG classes (Assert, @Test, @BeforeMethod, @AfterMethod), and WebDriverManager. IntelliJ adds these for you with Alt+Enter whenever you reference an unimported class.
WebDriver driver; — a class-level field. WebDriver is the interface (the contract); ChromeDriver is one implementation. Programming to the interface (not the concrete class) is what lets you swap browsers later by changing one line.
@BeforeMethod public void setup() — TestNG runs this before every @Test method in the class. The browser is created fresh per test, which guarantees test isolation: the second test never inherits state from the first.
WebDriverManager.chromedriver().setup(); — looks up the installed Chrome version on this machine and downloads (or finds in cache) the matching chromedriver binary. Without this line, you'd see a SessionNotCreatedException because Selenium can't find a driver. (As of Selenium 4.6+, the built-in Selenium Manager does the same thing automatically — but WebDriverManager is still the most widely deployed solution.)
driver = new ChromeDriver(); — actually starts a Chrome window. The driver binary launches Chrome, opens a debugger port, and your Java code now controls that browser via HTTP.
driver.manage().window().maximize(); — full-screens the browser. Helpful in CI where the default window is small enough to hide responsive layouts.
@Test public void shouldDisplayWelcomeTitle() — TestNG sees the annotation and treats this method as a test case. Method names are conventionally shouldDoSomething or verifySomething — start with a verb that describes the expected behaviour.
driver.get(url) — navigate to a URL. Synchronous: it blocks until the load event fires.
driver.getTitle() — returns the current page's <title> content as a String.
driver.findElement(By.linkText("Learn")) — find an element. By.linkText matches an <a> element by its visible text. We'll cover the full By.* family in chapter 2.
element.isDisplayed() — returns true if the element is rendered and visible. Useful for assertions about visibility.
Assert.assertTrue(condition, message) — TestNG's assertion. If the condition is false, the test fails and the message appears in the report. Always include a meaningful failure message — "true was false" doesn't help anyone debug.
@AfterMethod public void teardown() — runs after every @Test. driver.quit() closes every browser window the driver opened and releases the underlying browser process. Always quit. Always. The if (driver != null) guard handles the case where setup itself failed.
The lifecycle, visualised
The pattern repeats once per @Test. If your class has ten tests, the browser opens and closes ten times. That's slow — about 1–2 seconds per browser start — but it gives you bulletproof isolation. Chapter 5 introduces @BeforeClass for shared setup when speed matters more than full isolation.
Running the test three ways
You can launch the same class from three places, and you'll use all three at different times:
- IntelliJ play button — fastest during development. Right-click the file or class → Run. IntelliJ shows a tree of test results below the editor; click any failure to jump to the line.
- Maven on the command line —
mvn test -Dtest=HomePageTest. This is what CI runs. If a test fails in IntelliJ but not via Maven (or vice versa), the discrepancy is almost always classpath-related — Maven uses exactly the dependencies declared inpom.xml; IntelliJ may pull from extra sources. - TestNG XML suite —
mvn testwith the<suiteXmlFiles>config we set up in lesson 2. This is how you run a curated set of classes: smoke suite, regression suite, nightly suite. Covered in chapter 5.
A note on the boilerplate
If you're coming from Cypress, this looks heavy:
// Cypress equivalent
describe("Home page", () => {
it("displays the welcome title", () => {
cy.visit("https://qa.codes");
cy.title().should("contain", "qa.codes");
});
});Six lines vs forty. Yes. That's the trade-off of Java's explicitness. Every line in the Selenium version says exactly what it does — there's no implicit driver, no implicit teardown, no global cy object. Once you've typed the boilerplate twenty times, your fingers stop noticing it. By chapter 6 we'll factor most of it into a BaseTest class so individual tests look almost as terse as Cypress.
The flip side: when something breaks at 3am in production, the Java/Selenium codebase is unambiguous about what runs and when. There's no magic.
Comparing with Playwright (Java)
Playwright also has a Java client. Its first-test ceremony is similar in length but feels different in tone:
// Playwright equivalent — for comparison only
try (Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage()) {
page.navigate("https://qa.codes");
assertThat(page).hasTitle(Pattern.compile("qa.codes"));
}Playwright leans on try-with-resources for cleanup (no @AfterMethod needed) and on built-in web-first assertions (assertThat(page).hasTitle(...)) that retry automatically. Selenium's lifecycle is more explicit. Both approaches work — and once you've written one, you can read both.
⚠️ Common mistakes
- Forgetting
driver.quit(). Each unclosed driver leaves a browser process and a driver process alive. On CI runners with limited memory, fifty leaked processes will OOM the box. Always pair@BeforeMethod/new ChromeDriverwith@AfterMethod/driver.quit(). (Don't usedriver.close()— that closes only the current window; on a multi-window test, browser processes leak.) - Asserting after
driver.quit(). If you put assertions in@AfterMethod, the driver is already gone — everyfindElementcall throws. Keep assertions inside@Test. - Confusing
WebDriverandChromeDriver. DeclaringChromeDriver driver = new ChromeDriver();works but locks you in.WebDriver driver = new ChromeDriver();lets you swap tonew FirefoxDriver()later by changing one line. Always type the field as the interface.
🎯 Practice task
Run real Selenium against a real site. 30–40 minutes.
- Build on the Maven project from lesson 2. Create
src/test/java/com/mycompany/tests/tests/HomePageTest.javaand paste in the full class from the top of this lesson. - Run it from IntelliJ. Watch Chrome open, navigate, run, and close — twice. Both tests should be green.
- Now run it from the command line:
mvn clean test -Dtest=HomePageTest. The Surefire plugin runs the same two tests; you'll see two[INFO]lines and aBUILD SUCCESSsummary. - Make it fail on purpose. Change
Assert.assertTrue(title.contains("qa.codes"), ...)toAssert.assertTrue(title.contains("BOGUS"), ...). Run again. Read the IntelliJ failure pane — note that it shows the actual title in the message. Restore the assertion and confirm green. - Add a third test. Write
shouldDisplayCheatSheetsLinkthat visits qa.codes and asserts that an element with link text "Cheat Sheets" is displayed. Run all three; all three should pass. - Make it real. Pick any small public site you regularly use (Wikipedia, a personal portfolio, a Sauce Demo deployment). Write a fourth test that visits it and asserts something meaningful — a heading is visible, the title contains a known word. The point: prove to yourself you can do this on any site, not just the example.
- Stretch: convert the class to use
@BeforeClassinstead of@BeforeMethod(and@AfterClassinstead of@AfterMethod). Run it and watch the browser open once, run all three tests, and close once. Note the speedup. Then think about what could break — what state from test #1 leaks into test #2? That trade-off becomes a real design decision in chapter 5.
Next lesson: drivers in depth. The WebDriverManager.chromedriver().setup() line you typed today hides a lot — we'll unpack it, cover Firefox and Edge, and add browser options like headless mode for CI.