beginner19 min

Driver Lifecycle and Navigation

What a WebDriver instance actually represents, structuring its lifecycle with JUnit annotations, and the navigation methods beyond a simple get().

What you'll learn

  • Structure driver creation and teardown using JUnit's @BeforeEach/@AfterEach lifecycle
  • Use WebDriver's navigation methods (get, back, forward, refresh) correctly
  • Explain why a shared driver instance across tests risks state leaking between them

Prerequisites

Explanation

A WebDriver instance represents one real, running browser session — created (new ChromeDriver()), used for a series of commands, then closed (driver.quit()). The previous lesson's guided local lab used a manual try/finally inside one test method; a real, multi-test suite structures this with JUnit lifecycle annotations instead: @BeforeEach void setup() { driver = new ChromeDriver(); } creates a fresh driver before every single test method, and @AfterEach void teardown() { driver.quit(); } guarantees cleanup after every one, regardless of that test's outcome — JUnit runs @AfterEach even when a test method throws, giving you the same guaranteed-cleanup property as finally, but applied automatically across an entire test class rather than written by hand in each method.

This per-test fresh driver pattern matters for the same fundamental reason Playwright's per-test context isolation matters (if you've taken that course): a shared driver instance reused across multiple tests can leak state — cookies, browser history, whatever page the previous test happened to leave the browser on — silently affecting a later test's starting conditions in ways that make failures hard to reproduce and diagnose. The cost is real (launching a fresh browser per test is slower than reusing one), but the reliability and diagnosability payoff is why it's the standard, recommended default for correctness-focused test suites, not merely a performance-agnostic style preference.

Navigation goes beyond driver.get(url) (load a URL, waiting for the document.readyState to reach "complete" by default): driver.navigate().to(url) is functionally equivalent to get(); driver.navigate().back() and .forward() move through the browser's real history stack, exactly like clicking a real back/forward button; driver.navigate().refresh() reloads the current page. These matter specifically for testing workflows that depend on real browser history behavior — confirming a "back" button correctly returns a user to a previous, valid state, rather than an error page or a broken partial render, is a genuine, common test scenario these methods exist to support.

Example

Modeling the per-test-fresh-driver lifecycle and navigation-history stack, without a real browser.

class FakeDriverSession {
  constructor() { this.history = []; this.historyIndex = -1; this.cookies = new Set(); }
  get(url) {
    this.history = this.history.slice(0, this.historyIndex + 1);
    this.history.push(url);
    this.historyIndex++;
  }
  back() { if (this.historyIndex > 0) this.historyIndex--; }
  forward() { if (this.historyIndex < this.history.length - 1) this.historyIndex++; }
  currentUrl() { return this.history[this.historyIndex]; }
}

// beforeEach: a FRESH session per test -- no leaked cookies/history from a previous test.
function runTest(testBody) {
  const driver = new FakeDriverSession(); // fresh every time
  testBody(driver);
  // afterEach equivalent: driver discarded here, nothing carries over
}

runTest((driver) => {
  driver.get("/page-a");
  driver.get("/page-b");
  driver.back();
  console.log(driver.currentUrl()); // "/page-a" -- real history navigation
});

Try it yourself

Call driver.forward() after the back() call above and confirm it returns to /page-b.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write freshDriverPerTest(testNames, runTest) modeling JUnit's @BeforeEach/@AfterEach: for EACH name in testNames, create a fresh 'driver' object ({cookies: new Set()}), call runTest(driver, name), then discard it. Return an array of booleans: true if that test's driver had ZERO cookies at the start (proving no leakage from a previous test).

Checks: every test starts with a genuinely fresh, cookie-free driver · handles an empty test-names list

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write navigationHistory(actions) where actions is an array of {type: 'get'|'back'|'forward', url?} objects. Simulate them in order against a history stack (starting empty, index -1) and return the final current URL (or null if history is empty).

Checks: back correctly returns to a previous page · forward correctly returns to a page navigated back from · handles no actions at all · a new navigation after going back truncates any forward history

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Sharing one WebDriver instance across multiple tests to save time -- this risks leaked cookies, history, or page state silently affecting a later test's starting conditions, making failures hard to reproduce.
  • Putting driver.quit() inside @Test methods individually instead of a shared @AfterEach -- this duplicates cleanup logic across every test and risks a missed cleanup if any single test forgets it.
  • Assuming driver.navigate().back() is purely cosmetic -- it exercises the browser's REAL history mechanism, which is exactly what makes it useful for testing that a back button leads to a valid, correctly-rendered state, not an error.

Knowledge check

Knowledge check

1. Why does JUnit's @AfterEach guarantee driver.quit() runs even when a test method throws an exception?
2. What is the main risk of reusing one WebDriver instance across multiple tests instead of creating a fresh one per test?
3. What does driver.navigate().back() actually exercise?

Takeaway

Structure driver creation/teardown with JUnit's @BeforeEach/@AfterEach for guaranteed, automatic cleanup across every test in a class — a fresh driver per test avoids state leaking between tests, and navigation methods beyond get() exercise the browser's real history mechanism.

Summary

@BeforeEach creates a fresh WebDriver before every test; @AfterEach guarantees driver.quit() runs after every test regardless of outcome. A shared driver across tests risks leaked state affecting later tests. navigate().back()/forward()/refresh() exercise the browser's real history stack, not a simulated re-fetch.

References

Your notes

Notes save automatically.