ScreenshotNeo

BlogHow-to

JUnit Test Cases: How to Write and Run Them

Write a working JUnit Jupiter test, add it to a Java project, and run it from an IDE, Gradle, Maven, or the JUnit Console Launcher.

By the ScreenshotNeo team4 October 202610 min read

A JUnit test case is a Java method marked with @Test that calls code under test and checks the result with an assertion. Here is a complete Jupiter example:

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

import org.junit.jupiter.api.Test;

class CalculatorTest {
    @Test
    void addsTwoNumbers() {
        Calculator calculator = new Calculator();
        int result = calculator.add(1, 1);
        assertEquals(2, result);
    }
}

The assertion compares the expected value (2) with the actual value (result). If they differ, the test fails and reports the mismatch. This guide uses JUnit Jupiter, the programming model most Java developers mean when they say JUnit 5. For release-specific details, consult the JUnit 5.12.0 User Guide; check compatibility before selecting a JUnit release for your project.

1. What JUnit test cases do

A unit test checks one behavior of a unit of code, often a method or a small class. A test should make its setup, action, and expected outcome easy to see:

  1. Arrange: create inputs and the object or dependencies needed.
  2. Act: call the behavior being tested.
  3. Assert: verify the result or side effect.

JUnit 5 has three parts: the JUnit Platform discovers and launches tests, Jupiter provides the programming and extension model, and Vintage is an engine that lets the Platform run legacy JUnit 3 and JUnit 4 tests. New Jupiter tests use org.junit.jupiter imports. Do not mix those annotations with JUnit 4’s org.junit.Test in a beginner example. The JUnit 5.12.0 guide documents Java 8 or later as its runtime requirement; verify the chosen release’s current Java compatibility.

2. Create a small Java project and test

In a standard Gradle or Maven project, production code belongs under src/main/java and tests under src/test/java, with matching package names. Create these files for the example:

// src/main/java/example/Calculator.java
package example;

public class Calculator {
    public int add(int left, int right) {
        return left + right;
    }
}
// src/test/java/example/CalculatorTest.java
package example;

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

import org.junit.jupiter.api.Test;

class CalculatorTest {
    @Test
    void addsTwoNumbers() {
        Calculator calculator = new Calculator();
        int result = calculator.add(1, 1);
        assertEquals(2, result);
    }
}

The test class does not need to be public in Jupiter. A descriptive method name helps the IDE and build report explain what failed. A test should be independently runnable; avoid relying on another test to run first.

3. Choose assertions that describe the behavior

JUnit’s assertions are static methods on Assertions, usually imported individually. The expected value comes before the actual value in Jupiter:

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

assertEquals("ready", status);
assertTrue(items.isEmpty());
assertFalse(user.isLocked());
assertNull(optionalValue);
assertNotNull(createdObject);
assertArrayEquals(new int[] {1, 2}, actualValues);

Prefer a focused assertion that describes the contract. Add a failure message when it will make a failure easier to diagnose: assertEquals(expected, actual, "total includes tax"). Assertions stop that test method at the first failure; use separate tests for independent behaviors so each failure points to one issue.

Check exceptions explicitly

To verify that invalid input throws the documented exception, wrap only the operation expected to fail:

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

@Test
void rejectsNegativeQuantity() {
    IllegalArgumentException error = assertThrows(
        IllegalArgumentException.class,
        () -> new Order(-1)
    );
    assertEquals("quantity must be non-negative", error.getMessage());
}

assertThrows accepts the expected type or a subtype. Use assertThrowsExactly when the exact runtime type is part of the contract. If nothing throws, or an unrelated exception occurs, the assertion fails.

Use assumptions for environment-dependent tests

An assumption expresses a precondition for a test, such as a required environment variable. If the precondition is false, the test is aborted rather than counted as an ordinary assertion failure. Do not use assumptions to hide a product defect or routinely skip tests.

4. Set up and clean up test state

Use lifecycle methods when repeated setup or cleanup is clearer than repeating it in each test. @BeforeEach runs before each test method; @AfterEach runs afterward, including when the test fails.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class CartTest {
    private Cart cart;

    @BeforeEach
    void createCart() {
        cart = new Cart();
    }

    @AfterEach
    void cleanUp() {
        cart.close(); // only if the resource needs closing
    }

    @Test
    void startsEmpty() {
        // assertions about cart
    }
}

@BeforeAll and @AfterAll run once for the test class and are useful for genuinely expensive shared setup or teardown. By default, their methods must be static. Jupiter can use a per-class test instance lifecycle to allow instance methods, but shared mutable state can make tests order-dependent. Keep setup small and isolated where possible.

5. Test several inputs with parameterized tests

Parameterized tests run one test method with multiple arguments. The JUnit guide describes them as a way to run a test method multiple times with different arguments. Add the junit-jupiter-params artifact, then use an argument source such as @CsvSource:

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

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class CalculatorTest {
    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "-2, 2, 0",
        "0, 5, 5"
    })
    void addsValues(int left, int right, int expected) {
        assertEquals(expected, new Calculator().add(left, right));
    }
}

Use @ValueSource for one argument per case, @EnumSource for enum values, @CsvSource for compact rows of values, and @MethodSource for richer or computed arguments. The argument count and types must match the method parameters; conversion is supported for common types. Keep examples representative: ordinary values, boundaries, and invalid cases often reveal more than many arbitrary inputs.

6. Add JUnit to the build

JUnit has separate artifacts for its API, execution engine, and optional parameterized tests. The versioned examples below use JUnit 5.12.0 and its BOM to align JUnit 5 artifact versions. If Spring Boot or another framework manages versions, follow that framework’s dependency management instead of overriding it blindly.

Gradle Groovy DSL

// build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform('org.junit:junit-bom:5.12.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testImplementation 'org.junit.jupiter:junit-jupiter-params' // only for parameterized tests
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

Gradle Kotlin DSL

// build.gradle.kts
plugins {
    java
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(platform("org.junit:junit-bom:5.12.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testImplementation("org.junit.jupiter:junit-jupiter-params") // only for parameterized tests
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}

Maven

For Maven, use a current project setup and check the configured Maven Surefire version and JUnit engine. Plugin behavior and coordinates are version-sensitive; do not paste plugin versions from an unrelated old guide. The official guide links a JUnit sample project and describes Maven configuration.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>5.12.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

For parameterized tests, add org.junit.jupiter:junit-jupiter-params with test scope. Ensure the project uses a Surefire configuration that supports the selected JUnit Platform generation and includes a compatible engine. When in doubt, start from the official guide’s Maven setup and sample project.

7. Run the test cases

Run path Good for How to start
IDE Running one test while editing Use the gutter/run action beside a test method or class after the IDE imports the project.
Build tool Repeatable whole-project runs and CI Use the repository’s Gradle or Maven wrapper.
Console Launcher Platform execution without IDE support Use the standalone launcher JAR and select or scan the test classpath.

Run with Gradle

./gradlew test
# Windows
 gradlew.bat test

Run one test class with ./gradlew test --tests example.CalculatorTest. The Gradle test task must call useJUnitPlatform() so it uses the JUnit Platform. Use the project wrapper to keep local and CI Gradle versions consistent.

Run with Maven

./mvnw test
# Windows
mvnw.cmd test

Use the Maven wrapper if the repository provides one. To run a single class, Maven Surefire commonly accepts ./mvnw -Dtest=CalculatorTest test; the exact filtering behavior depends on the configured plugin.

Run from an IDE

Open or import the project as a Gradle or Maven project, allow dependency resolution, then run the test class or an individual method from the editor’s test gutter or test explorer. IDE support varies by version and configuration; if tests appear in the IDE but not in the build, check both configurations independently. The official JUnit guide documents support for IntelliJ IDEA, Eclipse, NetBeans, and Visual Studio Code.

Run with the Console Launcher

The JUnit Platform Console Launcher is useful when the IDE does not provide the needed support. Download the standalone JAR matching the Platform version from the official JUnit release artifacts, compile production and test classes with JUnit on the classpath, then run the launcher:

java -jar junit-platform-console-standalone-1.12.0.jar execute \
  --class-path build/classes/java/main:build/classes/java/test \
  --select-class example.CalculatorTest

Use a semicolon between classpath entries on Windows. The class directories above match a Gradle Java project; Maven’s output directories are usually under target/classes and target/test-classes. The standalone launcher is typically invoked directly rather than added as a project dependency. See the Console Launcher reference for selectors and options.

8. Common discovery and failure problems

Symptom Likely cause Fix
No tests found Test file is outside the configured test source set, class or method is not recognized, or no matching engine is present. Place the test under src/test/java; check @Test import and naming/filter; confirm Jupiter engine is on the test runtime classpath.
Test method runs as ordinary code but not as a test JUnit 4 annotation was imported, or the runner does not use the Platform. For Jupiter, import org.junit.jupiter.api.Test; configure Gradle with useJUnitPlatform() or verify Maven’s test plugin setup.
package org.junit.jupiter.api does not exist JUnit dependency is absent, scoped incorrectly, or dependency resolution failed. Add Jupiter as a test dependency, reload the build project, and inspect dependency resolution output.
Parameterized annotation or provider is missing The params artifact is not included. Add junit-jupiter-params in test scope and refresh dependencies.
IDE run button is missing or fails The IDE lacks Platform support for the project’s configuration or has stale project metadata. Import through the build tool, refresh dependencies, update IDE support, or run the wrapper task.
Old JUnit 4 tests disappear Only the Jupiter engine is configured. Keep using JUnit 4 where needed and add the Vintage engine to run those legacy tests on the Platform; migrate imports deliberately.
Expected and actual values look reversed Assertion arguments were ordered using a different library’s convention. In Jupiter, use assertEquals(expected, actual); put an optional failure message last.
Works locally, fails in CI Different Java version, environment, working directory, locale, timezone, or external service state. Use the project wrapper, pin the supported Java toolchain, make test inputs deterministic, and avoid depending on machine-specific state.
Intermittent failures Tests share mutable state, depend on execution order, use timing assumptions, or contact unreliable external systems. Isolate state, control clocks and data, use bounded waits, and test external integrations at an appropriate boundary.

9. Reliable, fast test suites

  • Keep unit tests deterministic: avoid current time, random data, network access, and machine-specific paths unless controlled.
  • Use the narrowest useful setup: expensive shared fixtures can save startup time but may leak state between tests.
  • Run focused tests while iterating: run one method or class, then run the full suite before committing or merging.
  • Keep CI repeatable: use the repository wrapper and the same declared JDK and dependency configuration as local development.
  • Diagnose rather than retry blindly: retrying flaky tests can mask a race or hidden dependency.

JUnit itself does not make a test suite fast or reliable; those properties depend on test design, fixtures, and external dependencies. Keep the quick feedback path small and run slower integration checks separately where the build permits.

10. Or skip the browser setup

JUnit checks Java behavior; it does not capture browser pages. If a development workflow also needs website screenshots for visual review or reports, ScreenshotNeo is a screenshot API and MCP server. Make a single GET request; see the ScreenshotNeo API documentation for options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

11. Frequently asked questions

Is JUnit 5 the same as Jupiter?

No. JUnit 5 names the overall generation; Jupiter is its programming model and test engine. The Platform discovers and launches engines.

Do I need to make the test class or method public?

Not for the Jupiter example shown here. Follow the access rules of the test framework and build setup you are using.

Should every method have its own test?

Write tests around meaningful behaviors and outcomes. A method can have several cases, and related methods can sometimes be verified through a public behavior.

Can I keep JUnit 4 tests while adding Jupiter?

Yes. The Platform can run legacy JUnit 3 and JUnit 4 tests through the Vintage engine, while Jupiter runs Jupiter tests. Configure the engines your project actually needs.

Where can I check version-specific details?

Use the versioned JUnit User Guide that matches the artifacts in your build, especially for dependencies, Java support, and launcher configuration.