JUnit 5 Basics — Your First Java Test
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-jupiteris an aggregator that includesjunit-jupiter-api,junit-jupiter-engine, andjunit-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
- JUnit 5 = Platform (engine) + Jupiter (API) + Vintage (backward compat).
- Tests live in
src/test/java— same package as code under test. - Each test method is annotated with
@Testand runs in a fresh instance. - Run with IDE (▶ button) or
mvn test. - Follow Arrange-Act-Assert pattern in every test.
What’s Next?
Next: JUnit 5 Annotations — lifecycle hooks, display names, ordering, tagging, timeouts, and more.