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:
<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:
| Rule | Description |
|---|---|
| Location | src/test/java |
| Class name | Ends with Test, starts with Test, or ends with TestCase |
| Method annotations | @Test marks a test method |
| Access modifier | Test classes and methods should be public or package-private |
Create a test class at src/test/java/com/example/CalculatorTest.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:
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:
# Compile and execute all tests
mvn testMaven compiles both main and test sources, then the Surefire plugin runs every discovered test. You will see output like this:
[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 SUCCESSViewing 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
| Annotation | Purpose |
|---|---|
@Test | Marks a method as a test case |
@BeforeEach | Runs before every test method in the class |
@AfterEach | Runs after every test method |
@BeforeAll | Runs once before any test in the class (must be static) |
@AfterAll | Runs once after all tests in the class (must be static) |
@DisplayName | Provides a human-readable name for the test in output |
@Disabled | Skips the test |
A More Complete Example
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:
| Method | Use 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
@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
<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
| Symptom | Likely Cause | Fix |
|---|---|---|
mvn test succeeds but IDE shows red tests | IDE test runner uses different classpath | Reload Maven project and invalidate IDE caches |
ClassNotFoundException during tests | Missing test-scope dependency | Check that the library is declared with <scope>test</scope> or compile |
| Tests pass individually but fail together | Shared mutable state between tests | Use @BeforeEach to reset state, avoid static mutable fields |
| Surefire reports 0 tests run | Test class name does not match *Test pattern | Rename the class, or configure <includes> in Surefire |
assertEquals fails with cryptic message | Objects 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:
# Run one test class
mvn test -Dtest=CalculatorTest
# Run one specific method
mvn test -Dtest=CalculatorTest#shouldAddTwoNumbersWhat 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?
# Skip test execution (still compiles test sources)
mvn package -DskipTests
# Skip both test execution and compilation
mvn package -Dmaven.test.skip=trueWhy 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.