Testing with Maven

Testing is not an afterthought in Maven — it is baked into the default lifecycle. Every time you run mvn test, Maven compiles your test sources, executes them, and reports the results. This chapter shows you how to set up JUnit 5, write your first test, and interpret the output.

Prerequisites

  • A Maven project with standard directory structure
  • Basic understanding of Java classes and methods

Setting Up JUnit 5

JUnit 5 (also called JUnit Jupiter) is the current generation of the most popular Java testing framework. To use it, add the JUnit Jupiter dependency to your pom.xml with test scope:

xml
<dependencies>
    <!-- JUnit 5 for unit testing -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

The test scope means this dependency is only available during test compilation and execution. It will not end up in your production JAR.

Tip

Use a BOM for Version Management

If you have multiple JUnit-related dependencies, import the JUnit BOM in <dependencyManagement> to keep versions in sync. For a single junit-jupiter artifact, hard-coding the version is fine.

After saving pom.xml, reload the Maven project in your IDE so the dependency is downloaded.

Test Class Conventions

Maven's maven-surefire-plugin discovers tests automatically if you follow these rules:

RuleDescription
Locationsrc/test/java
Class nameEnds with Test, starts with Test, or ends with TestCase
Method annotations@Test marks a test method
Access modifierTest classes and methods should be public or package-private

Create a test class at src/test/java/com/example/CalculatorTest.java:

java
package com.example;
 
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
 
class CalculatorTest {
 
    @Test
    void shouldAddTwoNumbers() {
        // Create the object under test
        Calculator calculator = new Calculator();
 
        // Invoke the method and capture the result
        int result = calculator.add(2, 3);
 
        // Assert the expected outcome
        assertEquals(5, result);
    }
}

And the matching implementation at src/main/java/com/example/Calculator.java:

java
package com.example;
 
public class Calculator {
 
    public int add(int a, int b) {
        // Return the sum of two integers
        return a + b;
    }
}

Running Tests

From the project root, run:

bash
# Compile and execute all tests
mvn test

Maven compiles both main and test sources, then the Surefire plugin runs every discovered test. You will see output like this:

text
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.CalculatorTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Time elapsed: 0.045 s
[INFO]
[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS

Viewing Detailed Reports

Surefire generates HTML and XML reports in target/surefire-reports/. Open target/surefire-reports/com.example.CalculatorTest.txt for a plain-text summary, or browse the HTML files for a formatted view.

Common JUnit 5 Annotations

AnnotationPurpose
@TestMarks a method as a test case
@BeforeEachRuns before every test method in the class
@AfterEachRuns after every test method
@BeforeAllRuns once before any test in the class (must be static)
@AfterAllRuns once after all tests in the class (must be static)
@DisplayNameProvides a human-readable name for the test in output
@DisabledSkips the test

A More Complete Example

java
package com.example;
 
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
 
@DisplayName("Calculator Operations")
class CalculatorTest {
 
    // Shared instance for all tests in this class
    private Calculator calculator;
 
    @BeforeEach
    void setUp() {
        // Initialize a fresh calculator before each test
        calculator = new Calculator();
    }
 
    @Test
    @DisplayName("Should add two positive numbers")
    void shouldAddTwoNumbers() {
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }
 
    @Test
    @DisplayName("Should subtract correctly")
    void shouldSubtract() {
        int result = calculator.subtract(5, 3);
        assertEquals(2, result);
    }
 
    @Test
    @DisplayName("Should return true for even numbers")
    void shouldDetectEvenNumbers() {
        boolean isEven = calculator.isEven(4);
        assertTrue(isEven);
    }
 
    @Test
    @DisplayName("Should throw exception on division by zero")
    void shouldThrowOnDivisionByZero() {
        // Verify that the expected exception is thrown
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
    }
 
    @Test
    @Disabled("Not implemented yet")
    void shouldCalculatePower() {
        // Placeholder for future test
    }
}

Essential Assertions

JUnit 5 provides a rich set of assertion methods in org.junit.jupiter.api.Assertions:

MethodUse Case
assertEquals(expected, actual)Two values are equal
assertTrue(condition)Condition evaluates to true
assertFalse(condition)Condition evaluates to false
assertNull(object)Object is null
assertNotNull(object)Object is not null
assertThrows(Exception.class, executable)Code throws a specific exception
assertAll(executables...)Group multiple assertions; all run even if one fails

Grouping Assertions

java
@Test
void shouldValidatePerson() {
    Person person = new Person("Alice", 30);
 
    // All assertions run; failure messages are collected
    assertAll(
        () -> assertEquals("Alice", person.getName()),
        () -> assertEquals(30, person.getAge()),
        () -> assertNotNull(person.getId())
    );
}

Test Coverage with JaCoCo

Test coverage measures how much of your production code is exercised by tests. It is a useful metric, though not a perfect one — 100% coverage does not guarantee bug-free code.

Adding JaCoCo

xml
<build>
    <plugins>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.11</version>
            <executions>
                <execution>
                    <goals>
                        <!-- Attach JaCoCo agent before tests run -->
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <execution>
                    <id>report</id>
                    <phase>test</phase>
                    <goals>
                        <!-- Generate HTML report after tests complete -->
                        <goal>report</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Run mvn test and then open target/site/jacoco/index.html in a browser. You will see a breakdown of line coverage, branch coverage, and complexity for each class.

Warning

Do Not Worship the Number

Coverage is a guide, not a goal. A project with 90% coverage and meaningless assertions is worse than a project with 70% coverage and thoughtful, edge-case-focused tests.

Troubleshooting Test Failures

SymptomLikely CauseFix
mvn test succeeds but IDE shows red testsIDE test runner uses different classpathReload Maven project and invalidate IDE caches
ClassNotFoundException during testsMissing test-scope dependencyCheck that the library is declared with <scope>test</scope> or compile
Tests pass individually but fail togetherShared mutable state between testsUse @BeforeEach to reset state, avoid static mutable fields
Surefire reports 0 tests runTest class name does not match *Test patternRename the class, or configure <includes> in Surefire
assertEquals fails with cryptic messageObjects lack a useful toString()Override toString() in your domain objects, or supply a custom message as the third argument

FAQ

Can I run a single test method from Maven?

Yes:

bash
# Run one test class
mvn test -Dtest=CalculatorTest
 
# Run one specific method
mvn test -Dtest=CalculatorTest#shouldAddTwoNumbers

What is the difference between @BeforeEach and @BeforeAll?

@BeforeEach runs before every single test method, giving each test a clean slate. @BeforeAll runs once before the entire class, which is useful for expensive setup like starting a test database. @BeforeAll methods must be static unless you annotate the test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS).

How do I skip tests during a build?

bash
# Skip test execution (still compiles test sources)
mvn package -DskipTests
 
# Skip both test execution and compilation
mvn package -Dmaven.test.skip=true

Why does my test fail with "No tests found"?

Make sure the test class is in src/test/java, the class name ends with Test, and the test methods are annotated with @Test. Also verify that the maven-surefire-plugin version supports JUnit 5 (3.0.0 or later).

Can I use JUnit 4 and JUnit 5 in the same project?

Yes, through the JUnit Vintage engine. Add junit-vintage-engine as a test dependency, and Maven will run both JUnit 4 and JUnit 5 tests together. This is useful when migrating a large codebase gradually.