JUnit Tutorial: Unit Testing in Java

JUnit Tutorial: Unit Testing in Java

Unit testing is non-negotiable in professional Java development. JUnit is the framework that makes it practical — and it has been the standard Java testing tool for over two decades. This tutorial walks you through everything you need to write, organize, and run JUnit tests, from the first dependency declaration to running a full test suite.

What Is JUnit and Why Does It Matter?

JUnit is an open-source unit testing framework for Java. It gives you the annotations, assertions, and test runner infrastructure needed to write automated tests that verify your code behaves correctly.

The case for JUnit is straightforward:

  • Instant feedback — run your test suite in seconds and know immediately if a change broke something
  • Safe refactoring — modify internals confidently when tests prove the behavior is preserved
  • Living documentation — well-named tests describe what the code is supposed to do
  • IDE and CI integration — every major Java IDE and CI system understands JUnit out of the box

JUnit is not a testing religion. It is a practical tool. Nearly every Java project you encounter will already use it, which makes knowing it a baseline professional skill.

JUnit 4 vs JUnit 5

JUnit 4 was released in 2006 and is still running in millions of projects. JUnit 5 (also called JUnit Jupiter) arrived in 2017 as a complete rewrite with a modular architecture and a significantly improved API.

Feature JUnit 4 JUnit 5
Java requirement Java 5+ Java 8+
Architecture Single jar Platform + Jupiter + Vintage
Annotations @Before, @After @BeforeEach, @AfterEach
Exception testing @Test(expected=...) assertThrows()
Display names Not supported @DisplayName
Parameterized tests Limited First-class support

Recommendation: use JUnit 5. If you are starting a new project or have the option to migrate, JUnit 5 is more expressive, better integrated with modern IDEs, and actively maintained. The rest of this tutorial covers JUnit 5.

Adding JUnit 5 to Your Project

Maven

Add the following to your pom.xml:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

The junit-jupiter artifact is an aggregator that pulls in the API, engine, and params modules. Maven Surefire 3.x discovers and runs JUnit 5 tests automatically.

Gradle

In build.gradle:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

test {
    useJUnitPlatform()
}

The useJUnitPlatform() call is required — without it, Gradle will not invoke the JUnit 5 engine.

Writing Your First Test

Given this simple calculator class:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int divide(int a, int b) {
        if (b == 0) throw new ArithmeticException("Cannot divide by zero");
        return a / b;
    }
}

Here is a JUnit 5 test class for it:

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

class CalculatorTest {

    @Test
    void addsTwoPositiveNumbers() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void addNegativeAndPositive() {
        Calculator calc = new Calculator();
        assertEquals(-1, calc.add(-4, 3));
    }

    @Test
    void divideThrowsOnZero() {
        Calculator calc = new Calculator();
        assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
    }
}

The key pieces:

  • @Test marks a method as a test case. No return type, no parameters (in basic usage).
  • assertEquals(expected, actual) — the most common assertion. Fails with a clear message if values differ.
  • assertTrue(condition) — passes when the condition is true.
  • assertThrows(ExceptionClass, executable) — verifies that the lambda throws the specified exception. Much cleaner than JUnit 4's expected attribute.

The Assertions class has more than a dozen methods: assertNull, assertNotNull, assertSame, assertAll (group assertions), and assertTimeout for performance constraints.

Test Lifecycle Annotations

JUnit 5 gives you four hooks to set up and tear down state around your tests.

import org.junit.jupiter.api.*;

class OrderServiceTest {

    private OrderService service;
    private static DatabaseConnection db;

    @BeforeAll
    static void initDatabase() {
        db = DatabaseConnection.open("jdbc:h2:mem:test");
    }

    @BeforeEach
    void setUp() {
        service = new OrderService(db);
    }

    @Test
    void createsOrderWithCorrectTotal() {
        Order order = service.create(List.of(new Item("Widget", 9.99)));
        assertEquals(9.99, order.getTotal(), 0.001);
    }

    @Test
    void rejectsEmptyOrder() {
        assertThrows(IllegalArgumentException.class,
            () -> service.create(Collections.emptyList()));
    }

    @AfterEach
    void tearDown() {
        service.reset();
    }

    @AfterAll
    static void closeDatabase() {
        db.close();
    }
}
  • @BeforeAll — runs once before any test in the class. Must be static. Use for expensive setup like database connections.
  • @BeforeEach — runs before every individual test. Use for per-test object initialization.
  • @AfterEach — runs after every test. Use to reset mutable state.
  • @AfterAll — runs once after all tests complete. Must be static. Use to release shared resources.

By default, JUnit creates a new instance of the test class for each test method. This means @BeforeEach fields are fresh for every test, which prevents tests from polluting each other.

@DisplayName for Readable Test Names

Test method names are constrained by Java identifier rules. @DisplayName lets you write a plain-English description that appears in IDE test results and reports.

import org.junit.jupiter.api.DisplayName;

@DisplayName("Shopping Cart")
class ShoppingCartTest {

    @Test
    @DisplayName("calculates total price including tax")
    void totalIncludesTax() {
        ShoppingCart cart = new ShoppingCart(0.08);
        cart.add(new Item("Book", 20.00));
        assertEquals(21.60, cart.getTotal(), 0.001);
    }

    @Test
    @DisplayName("returns zero for empty cart")
    void emptyCartReturnsZero() {
        ShoppingCart cart = new ShoppingCart(0.08);
        assertEquals(0.0, cart.getTotal());
    }
}

In IntelliJ and Eclipse, the test runner panel shows "Shopping Cart > calculates total price including tax" instead of ShoppingCartTest > totalIncludesTax. This matters when test failures need to be read by someone who did not write the code.

Organizing Your Tests

Package structure

Mirror your main source tree in the test source tree:

src/
  main/java/com/example/
    Calculator.java
    OrderService.java
  test/java/com/example/
    CalculatorTest.java
    OrderServiceTest.java

Keeping the same package means test classes can access package-private members without needing to make them public.

Test class conventions

  • One test class per production class, as a baseline
  • Name test classes <ClassName>Test by convention
  • Group related tests with nested classes annotated @Nested:
class UserServiceTest {

    @Nested
    @DisplayName("registration")
    class Registration {
        @Test
        void acceptsValidEmail() { ... }

        @Test
        void rejectsInvalidEmail() { ... }
    }

    @Nested
    @DisplayName("authentication")
    class Authentication {
        @Test
        void returnsTokenOnValidCredentials() { ... }
    }
}

@Nested classes appear as a hierarchy in the test runner, making it easy to see which area of behavior is failing.

Running Tests

Maven

# Run all tests
mvn <span class="hljs-built_in">test

<span class="hljs-comment"># Run a single test class
mvn <span class="hljs-built_in">test -Dtest=CalculatorTest

<span class="hljs-comment"># Run a single test method
mvn <span class="hljs-built_in">test -Dtest=CalculatorTest#addsTwoPositiveNumbers

<span class="hljs-comment"># Skip tests during build
mvn package -DskipTests

Surefire generates XML reports in target/surefire-reports/. HTML reports are available via the Surefire Report plugin.

Gradle

# Run all tests
gradle <span class="hljs-built_in">test

<span class="hljs-comment"># Run a single test class
gradle <span class="hljs-built_in">test --tests <span class="hljs-string">"com.example.CalculatorTest"

<span class="hljs-comment"># Run a single test method
gradle <span class="hljs-built_in">test --tests <span class="hljs-string">"com.example.CalculatorTest.addsTwoPositiveNumbers"

Gradle generates HTML reports in build/reports/tests/test/index.html automatically.

IDE Integration

IntelliJ IDEA

JUnit 5 support is built in with no additional plugins required. To run tests:

  • Click the green play button next to a test class or method
  • Right-click anywhere in a test file → Run
  • Use Ctrl+Shift+F10 (Windows/Linux) or Ctrl+R (Mac) to run the nearest test

The test runner panel shows pass/fail status, execution time, and failure details with stack traces. Failed assertions display the expected and actual values side by side.

To run all tests in a module: right-click the test source root → Run All Tests.

Eclipse

Eclipse requires the JUnit 5 support that comes bundled with recent versions (2019-03+). To run tests:

  • Right-click a test class → Run As → JUnit Test
  • Use Alt+Shift+X, T keyboard shortcut

The JUnit view at the bottom shows the test tree with green/red indicators. Eclipse also supports running individual methods by right-clicking inside the method body.

Both IDEs support re-running only failed tests — useful when a subset of your suite is broken and you want rapid feedback during a fix.


Automate Testing Beyond Unit Tests

JUnit covers your Java classes. For end-to-end browser testing with AI-generated tests and 24/7 monitoring, HelpMeTest handles the full stack — starting free.

Start testing free →

Read more