JUnit 5 Basics — Your First Java Test

2 minute read

JUnit 5 is the standard testing framework for Java. It’s what you’ll use for every project — from simple utilities to Spring Boot microservices. Let’s write your first test.


Maven Dependencies

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

<build>
    <plugins>
        <!-- Required for Maven to find JUnit 5 tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

junit-jupiter is an aggregator that includes junit-jupiter-api, junit-jupiter-engine, and junit-jupiter-params.


Project Structure

src/
├── main/java/com/myapp/
│   └── UserService.java
└── test/java/com/myapp/
    └── UserServiceTest.java

Convention: Test class name = class under test + Test suffix. Same package, different source root.


Your First Test

The code under test:

package com.myapp;

public class StringUtils {

    public static String reverse(String input) {
        if (input == null) throw new IllegalArgumentException("Input cannot be null");
        return new StringBuilder(input).reverse().toString();
    }

    public static boolean isPalindrome(String input) {
        String cleaned = input.toLowerCase().replaceAll("[^a-z0-9]", "");
        return cleaned.equals(new StringBuilder(cleaned).reverse().toString());
    }
}

The test:

package com.myapp;

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

class StringUtilsTest {

    @Test
    void shouldReverseString() {
        String result = StringUtils.reverse("hello");
        assertEquals("olleh", result);
    }

    @Test
    void shouldReturnEmptyForEmptyString() {
        assertEquals("", StringUtils.reverse(""));
    }

    @Test
    void shouldThrowExceptionForNull() {
        assertThrows(IllegalArgumentException.class, () -> {
            StringUtils.reverse(null);
        });
    }

    @Test
    void shouldDetectPalindrome() {
        assertTrue(StringUtils.isPalindrome("racecar"));
        assertTrue(StringUtils.isPalindrome("A man a plan a canal Panama"));
        assertFalse(StringUtils.isPalindrome("hello"));
    }
}

Running Tests

IntelliJ IDEA

  • Right-click test class → Run ‘StringUtilsTest’
  • Or click the green ▶ icon next to each test method

Maven

mvn test

Output:

[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Run a Single Test Class

mvn test -Dtest=StringUtilsTest

Run a Single Test Method

mvn test -Dtest=StringUtilsTest#shouldReverseString

JUnit 5 Architecture

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

┌─────────────────────────────────────────────┐
│              JUnit Platform                  │
│   (Foundation — discovers & runs tests)      │
├───────────────────┬─────────────────────────┤
│   JUnit Jupiter   │    JUnit Vintage        │
│   (JUnit 5 API)   │    (JUnit 3/4 compat)   │
└───────────────────┴─────────────────────────┘
Component Purpose
Platform Test discovery, execution, IDE/build tool integration
Jupiter JUnit 5 annotations, assertions, extensions
Vintage Run legacy JUnit 3/4 tests on JUnit 5 platform

Test Lifecycle

1. Instantiate test class (new instance per test method — default)
2. @BeforeEach  → runs before EACH test
3. @Test        → the test method
4. @AfterEach   → runs after EACH test
class LifecycleDemo {

    LifecycleDemo() {
        System.out.println("New instance created");
    }

    @BeforeEach
    void setUp() {
        System.out.println("Before each test");
    }

    @Test
    void testA() {
        System.out.println("Test A");
    }

    @Test
    void testB() {
        System.out.println("Test B");
    }

    @AfterEach
    void tearDown() {
        System.out.println("After each test");
    }
}

Output:

New instance created
Before each test
Test A
After each test
New instance created
Before each test
Test B
After each test

Key: JUnit 5 creates a new instance of the test class for each test method. This prevents shared state between tests.


Test Reports

Maven Surefire generates reports in target/surefire-reports/:

target/surefire-reports/
├── TEST-com.myapp.StringUtilsTest.xml
└── com.myapp.StringUtilsTest.txt

For HTML reports, add the Surefire Report plugin:

mvn surefire-report:report
# Open target/site/surefire-report.html

Common Mistakes

Mistake Fix
Test class or method is private Must be package-private or public
Missing @Test annotation JUnit won’t discover it
Old org.junit.Test (JUnit 4) Use org.junit.jupiter.api.Test
No Surefire plugin Add maven-surefire-plugin 3.x
Test method has parameters @Test methods can’t have parameters (use @ParameterizedTest)

Key Takeaways

  1. JUnit 5 = Platform (engine) + Jupiter (API) + Vintage (backward compat).
  2. Tests live in src/test/java — same package as code under test.
  3. Each test method is annotated with @Test and runs in a fresh instance.
  4. Run with IDE (▶ button) or mvn test.
  5. Follow Arrange-Act-Assert pattern in every test.

What’s Next?

Next: JUnit 5 Annotations — lifecycle hooks, display names, ordering, tagging, timeouts, and more.

Updated: