ScreenshotNeo

BlogHow-to

JUnit 5 Annotations in Selenium: Tutorial with Examples

Use JUnit Jupiter annotations to structure Selenium tests, manage WebDriver cleanup, and choose between isolated and shared browser sessions.

By the ScreenshotNeo team4 October 202611 min read

Use JUnit Jupiter’s @BeforeEach and @AfterEach to start and quit a Selenium WebDriver around each test. Put browser actions and assertions in @Test methods. This gives each test a clear lifecycle and helps prevent browser sessions and state from leaking between tests.

This tutorial uses JUnit Jupiter, the programming model introduced as part of JUnit 5. Its annotations live primarily in org.junit.jupiter.api. They are different from JUnit 4 annotations: do not mix org.junit.Test with Jupiter annotations in the same test class.

1. Add JUnit Jupiter and Selenium to the project

Your test needs Selenium’s Java bindings, JUnit Jupiter, and a browser and driver setup compatible with your Selenium release and CI environment. Selenium’s Java example uses ChromeDriver. Selenium’s driver-management behavior and browser compatibility can change, so check the documentation for the versions you choose.

For Maven, use the JUnit BOM to keep Jupiter artifacts on one version, and add the Jupiter engine so Maven Surefire can discover and run Jupiter tests. Add the Selenium Java artifact at a release compatible with your browser and environment. These are version properties to fill in with the releases selected for your project; check the corresponding release documentation before publishing or building.

<properties>
    <junit.version>5.12.0</junit.version>
    <selenium.version>REPLACE_WITH_COMPATIBLE_SELENIUM_VERSION</selenium.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>${junit.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>${selenium.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Replace the Selenium version placeholder before building. If your build already manages JUnit or Selenium versions, follow its existing dependency management instead. The example below follows Selenium’s published Java form example; it is not a claim that the example was executed in your environment. JUnit 5.12.0 User Guide · Selenium Java example.

2. Use the core JUnit 5 annotations in a Selenium test

This complete test starts a fresh Chrome session before each test and quits it afterward. The test opens Selenium’s sample web form, checks the page title, submits text, and checks the confirmation message.

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.Duration;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;

class WebFormTest {
    private WebDriver driver;

    @BeforeEach
    void setUp() {
        driver = new ChromeDriver();
    }

    @Test
    @DisplayName("submits text and shows a confirmation")
    void submitsTextAndShowsConfirmation() {
        driver.manage().timeouts().implicitlyWait(Duration.ofMillis(500));
        driver.get("https://www.selenium.dev/selenium/web/web-form.html");

        assertEquals("Web form", driver.getTitle());

        WebElement textBox = driver.findElement(By.name("my-text"));
        WebElement submitButton = driver.findElement(By.cssSelector("button"));
        textBox.sendKeys("Selenium");
        submitButton.click();

        assertEquals("Received!", driver.findElement(By.id("message")).getText());
    }

    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

The 500 millisecond implicit wait mirrors the value in Selenium’s example. It is not a universal wait recommendation. For asynchronously rendered UI, wait for the condition that matters to the test rather than adding arbitrary delays. If setup fails before assigning a driver, the null guard lets teardown finish without throwing another exception.

3. What each annotation does

Annotation When it runs Use in browser tests
@Test For one test method Navigate, perform one behavior, and assert its observable result.
@BeforeEach Before every test invocation Create a fresh driver and establish required test setup.
@AfterEach After every test invocation Call driver.quit() to end the browser session.
@BeforeAll Once before methods in the class Initialize class-wide resources when sharing them is intentional.
@AfterAll Once after methods in the class Release class-wide resources.
@DisplayName Labels a class or test in reports Describe the tested behavior in readable terms.
@ParameterizedTest Once for each provided argument set Exercise the same behavior with multiple inputs.
@RepeatedTest A requested number of repetitions Repeat a test; repetition alone does not vary test data.
@Nested Groups tests in an inner test class Organize related browser behaviors by page or feature.
@Tag Labels a test or class Mark suites such as smoke or slow for filtering.
@Disabled Prevents a test or class from running Temporarily disable a test with a reason, then remove the annotation when resolved.
@ExtendWith Registers a Jupiter extension Connect reusable framework integrations; a simple hand-written driver lifecycle does not require it.

4. What do @BeforeEach and @AfterEach do?

JUnit Jupiter’s default test-instance lifecycle creates a new test-class instance for each test method. The browser session is a separate external resource, however, so a new Java object does not close a browser automatically. @BeforeEach and @AfterEach make ownership explicit: the test class starts a session and then closes it after each invocation.

Use quit() to end the whole WebDriver session, including its associated windows. close() closes the current window and is not a replacement for ending the session. A driver created in @BeforeEach should normally be closed in @AfterEach, including when a test assertion fails.

5. Choose per-test or per-class browser lifecycle

Approach Advantages Costs and risks
Fresh driver per test with @BeforeEach/@AfterEach Simple ownership and stronger isolation; cookies, navigation, and windows begin from a new session. Each test pays browser startup time.
Shared driver per class with @BeforeAll/@AfterAll Can reduce repeated startup work. Cookies, windows, navigation, and mutable state can affect later tests; reset rules are necessary.

By default, @BeforeAll and @AfterAll methods must be static. Jupiter permits non-static class-level lifecycle methods when the class uses @TestInstance(TestInstance.Lifecycle.PER_CLASS). That mode also means all test methods share one test object, so mutable fields can carry state between methods.

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedBrowserTest {
    private WebDriver driver;

    @BeforeAll
    void startBrowser() {
        driver = new ChromeDriver();
    }

    @Test
    void firstPageCheck() {
        driver.get("https://www.selenium.dev/");
        // Assert the behavior needed by this test.
    }

    @Test
    void secondPageCheck() {
        // Reset navigation and any relevant browser state deliberately.
    }

    @AfterAll
    void stopBrowser() {
        if (driver != null) {
            driver.quit();
        }
    }
}

This shared-session example shows the lifecycle shape; its test bodies need application-specific assertions and reset steps. Prefer per-test ownership until startup cost is a measured concern and the team has defined how to reset session state.

6. Repeat a Selenium behavior with parameterized tests

@ParameterizedTest invokes one method for each argument set supplied by a source. The Jupiter params artifact must be present and aligned with the other Jupiter artifacts. This example is a complete parameterized form submission test using the same sample page:

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

class WebFormParameterizedTest {
    private WebDriver driver;

    @BeforeEach
    void setUp() {
        driver = new ChromeDriver();
    }

    @ParameterizedTest
    @ValueSource(strings = { "Selenium", "JUnit Jupiter" })
    void submitsEachTextValue(String input) {
        driver.get("https://www.selenium.dev/selenium/web/web-form.html");
        driver.findElement(By.name("my-text")).sendKeys(input);
        driver.findElement(By.cssSelector("button")).click();
        assertEquals("Received!", driver.findElement(By.id("message")).getText());
    }

    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Other useful sources include @CsvSource for rows of related values and @MethodSource for arguments assembled in Java. Choose inputs that represent meaningful cases such as a normal value, an empty value, or a boundary value. Parameterization is for data variation; use @RepeatedTest when the same invocation should run a fixed number of times.

7. Organize tests and reports

Use reporting and grouping annotations to communicate why a test exists:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@DisplayName("Checkout form")
class CheckoutTest {
    @Nested
    @DisplayName("when the form is valid")
    class ValidForm {
        @Test
        @Tag("smoke")
        @DisplayName("shows the order confirmation")
        void showsConfirmation() {
            // Browser steps and assertion for the application under test.
        }
    }
}

Keep tag names consistent across a project so build filters remain useful. A disabled test should explain why it is disabled and should have a path back to being enabled.

8. Synchronize with the page instead of guessing

Browser pages often render asynchronously. A fixed sleep can make a test slow when the page is fast and flaky when it is slower than expected. Synchronize on the condition the test needs. Selenium provides explicit waits; use a wait strategy appropriate to the application and avoid combining implicit and explicit waits without understanding the resulting behavior.

import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("message")));
String confirmation = driver.findElement(By.id("message")).getText();

The timeout is an upper bound for waiting, not a delay that is always added. Pick a value appropriate to the test environment and the UI condition, and make failures report which condition did not appear.

9. Common errors and fixes

Symptom Likely cause What to check or change
JUnit reports no tests found The Jupiter engine is missing, the build runner is not configured for Jupiter, or the class/method is outside the runner’s discovery conventions. Include the Jupiter engine, check the Maven or Gradle test runner configuration, and confirm the method imports org.junit.jupiter.api.Test.
Annotation import cannot be resolved The Jupiter API dependency is absent, or the code uses JUnit 4 imports while expecting Jupiter. Add Jupiter dependencies and consistently use org.junit.jupiter.api annotations.
Parameterized-test annotation is unresolved The params module is missing. Add junit-jupiter-params at the same version as the other Jupiter artifacts.
Browser or driver fails to start Browser installation, driver compatibility, permissions, or CI environment setup may not match the selected Selenium release. Check the Selenium release guidance and the browser availability and compatibility in the environment where the test runs.
Later tests see the wrong page or stale session state The driver is shared, or the session was not quit and recreated between tests. Use per-test setup and teardown, or define and apply explicit reset steps for a shared session.
Element lookup fails immediately The element is not yet present, the locator does not match, or navigation did not reach the expected page. Check the URL and locator, then wait for the relevant condition instead of increasing arbitrary sleeps.
Teardown throws after setup failed The driver was never assigned before teardown ran. Keep a null guard in teardown and close the driver only when it exists.
Browser window closes but the session remains close() was used instead of ending the session. Call quit() in teardown.
Jupiter lifecycle method signature is rejected A class-level hook is non-static while using the default per-method test instance lifecycle. Make @BeforeAll/@AfterAll static, or deliberately enable PER_CLASS.

10. Performance, reliability, and cost

  • Startup time: A new browser per test costs startup time but makes each test’s browser state easier to reason about. A shared class session may reduce startup work, with added reset and isolation responsibilities.
  • Failure diagnosis: Keep each test focused on one behavior, use meaningful display names, and wait for observable conditions. This makes failures easier to localize.
  • Cleanup reliability: Put quit() in @AfterEach or @AfterAll according to the session owner. Avoid relying on the test process ending to clean up browser resources.
  • CI compatibility: Pin compatible dependency versions in the build and verify browser availability and driver setup in the actual CI environment.
  • Cost: The main operational tradeoff in this pattern is browser execution time and the resources consumed by browser sessions. The dossier provides no benchmark or universal cost figure; measure your own suite and infrastructure.

11. Capture a visual record when debugging a browser flow

A Selenium assertion checks behavior directly. A screenshot can help a developer inspect the rendered page when a browser flow fails or when a visual record is useful, but it does not replace assertions.

ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-request API can capture a page as PNG, JPEG, WebP, or PDF. See the ScreenshotNeo site for the product and the ScreenshotNeo API documentation for request options.

12. FAQ

Does JUnit 5 mean JUnit Jupiter?

JUnit 5 is the release generation; Jupiter is its programming and extension model. The annotations in this guide are Jupiter annotations.

Can a parameterized test use @BeforeEach?

Yes. The setup and teardown lifecycle applies to each parameterized-test invocation, so a fresh driver can be created and quit for each argument set.

Should every Selenium test use a new browser?

It is a straightforward default for isolation. Sharing can be appropriate when startup cost matters and the suite has deliberate state-reset rules.

Do I need @ExtendWith to create a driver?

No. A test class can manage its driver directly with lifecycle methods. Use an extension when you need reusable lifecycle integration across test classes.

Or skip the browser setup

If your goal is to save a rendered page rather than run browser assertions, ScreenshotNeo can capture it with one HTTP request. This Java test example requests a WebP screenshot of the Selenium sample form:

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

class ScreenshotNeoCapture {
    public static void main(String[] args) throws IOException, InterruptedException {
        String accessKey = System.getenv("SCREENSHOTNEO_API_KEY");
        if (accessKey == null || accessKey.isBlank()) {
            throw new IllegalStateException("Set SCREENSHOTNEO_API_KEY first");
        }

        String targetUrl = "https://www.selenium.dev/selenium/web/web-form.html";
        String query = "access_key=" + URLEncoder.encode(accessKey, StandardCharsets.UTF_8)
                + "&url=" + URLEncoder.encode(targetUrl, StandardCharsets.UTF_8);
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.screenshotneo.com/v1/shot?" + query))
                .GET()
                .build();

        HttpResponse<byte[]> response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofByteArray());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new IOException("Screenshot request failed with HTTP " + response.statusCode());
        }
        Files.write(Path.of("shot.webp"), response.body());
    }
}

The API call does not perform Selenium interactions or assertions. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Read the API docs, then sign up for 1,000 free screenshots a month with no card.