JUnit 5 is the modern testing framework for Java. It helps you write unit tests, parameterized tests, and integration-friendly test suites with clean annotations and powerful extensions.
In this guide, you’ll learn JUnit 5 from scratch with practical examples, best practices, and real-world patterns.
What is JUnit 5?
JUnit 5 is the latest major version of JUnit. It is made of three parts:
- JUnit Platform: Launches tests on the JVM (IDE, Maven, Gradle).
- JUnit Jupiter: The new programming model + annotations (what you use most).
- JUnit Vintage: Runs older JUnit 3/4 tests (optional).
Keyword targets: JUnit 5 tutorial, JUnit Jupiter, JUnit 5 annotations, Java unit testing.
Why JUnit 5 is Better Than JUnit 4
- New annotations:
@BeforeEach,@AfterEach,@BeforeAll,@AfterAll - Better assertions:
assertAll,assertTimeout, lambda-basedassertThrows - Parameterized tests built-in
- Tags + nested tests + repeated tests
- Extension model replaces old runners (
@ExtendWith)
JUnit 5 Setup (Maven and Gradle)
Maven (JUnit 5 dependency + Surefire)
<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>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
</plugins>
</build>
Gradle (JUnit Platform)
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
test {
useJUnitPlatform()
}
Run tests:
- Maven:
mvn test - Gradle:
./gradlew test
JUnit 5 Test Class Basics
A JUnit 5 test method uses @Test.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void addsTwoNumbers() {
int sum = 10 + 20;
assertEquals(30, sum);
}
}
Use method names like shouldAddTwoNumbers() or addsTwoNumbers().
JUnit 5 Lifecycle Annotations
Lifecycle methods help you set up and clean up test state.
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void beforeAll() { /* runs once before all tests */ }
@AfterAll
static void afterAll() { /* runs once after all tests */ }
@BeforeEach
void beforeEach() { /* runs before each test */ }
@AfterEach
void afterEach() { /* runs after each test */ }
@Test
void sampleTest() {
assertTrue(true);
}
}
Note:
@BeforeAlland@AfterAllmust bestaticunless you use@TestInstance(Lifecycle.PER_CLASS).
JUnit 5 Assertions (Most Used)
Basic assertions
assertEquals(5, 2 + 3);
assertNotEquals(5, 2 + 2);
assertTrue(10 > 1);
assertFalse(1 > 10);
assertNull(null);
assertNotNull("hello");
Assert with message
assertEquals(10, value, "Value should be 10");
Grouped assertions with assertAll (great for DTO validation)
assertAll("user",
() -> assertEquals("Susil", user.getName()),
() -> assertEquals(40, user.getAge()),
() -> assertTrue(user.isActive())
);
Assert exceptions with assertThrows
var ex = assertThrows(IllegalArgumentException.class,
() -> Integer.parseInt("abc")
);
assertTrue(ex.getMessage() == null || ex.getMessage().length() >= 0);
Timeouts
assertTimeout(java.time.Duration.ofMillis(200), () -> {
Thread.sleep(50);
});
JUnit 5 Assumptions (Skip Tests Conditionally)
Assumptions abort a test when conditions don’t match (useful for OS/env-specific tests).
import static org.junit.jupiter.api.Assumptions.*;
@Test
void runsOnlyOnCi() {
assumeTrue("true".equals(System.getenv("CI")));
// test logic runs only in CI
}
Parameterized Tests in JUnit 5 (Essential for Real Projects)
Add dependency (if not using junit-jupiter BOM style):
org.junit.jupiter:junit-jupiter-params
@ValueSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class StringTest {
@ParameterizedTest
@ValueSource(strings = {"racecar", "madam", "level"})
void shouldBePalindrome(String word) {
assertTrue(new StringBuilder(word).reverse().toString().equals(word));
}
}
@CsvSource (best for business rules)
import org.junit.jupiter.params.provider.CsvSource;
@ParameterizedTest
@CsvSource({
"10, 20, 30",
"5, 7, 12"
})
void addsUsingCsv(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@MethodSource (complex data)
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
static Stream<Arguments> discountCases() {
return Stream.of(
Arguments.of(1000, 0.10, 900),
Arguments.of(200, 0.05, 190)
);
}
@ParameterizedTest
@MethodSource("discountCases")
void calculatesDiscount(int price, double rate, int expected) {
int finalPrice = (int)(price * (1 - rate));
assertEquals(expected, finalPrice);
}
Display Names, Ordering, and Tags
@DisplayName
@Test
@DisplayName("Should calculate GST correctly")
void gstTest() { }
@Tag (run subsets of tests)
import org.junit.jupiter.api.Tag;
@Test
@Tag("fast")
void fastTest() { }
Maven example:
- Run only fast tests:
mvn test -Dgroups=fast(tag filtering differs by setup; often done via Surefire configuration)
Nested Tests (Organize Like a Spec)
import org.junit.jupiter.api.*;
class AuthServiceTest {
@Nested
class Login {
@Test
void shouldFailForWrongPassword() { }
@Test
void shouldPassForCorrectPassword() { }
}
@Nested
class Signup {
@Test
void shouldRejectWeakPassword() { }
}
}
This structure reads like documentation and improves blog clarity too.
Repeated Tests
import org.junit.jupiter.api.RepeatedTest;
import static org.junit.jupiter.api.Assertions.*;
class RetryLikeTest {
@RepeatedTest(3)
void repeated() {
assertTrue(2 + 2 == 4);
}
}
Disable Tests (with reason)
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled("Waiting for bug fix in payment gateway sandbox")
@Test
void paymentSandboxTest() { }
Test Instance Lifecycle (PER_CLASS vs PER_METHOD)
Default is PER_METHOD (new instance per test). For expensive setup, sometimes you use PER_CLASS.
import org.junit.jupiter.api.TestInstance;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ExpensiveSetupTest {
// @BeforeAll can be non-static now
}
Use carefully: shared state can cause flaky tests.
JUnit 5 Best Practices (Real-World)
1) Follow AAA pattern
- Arrange: setup data
- Act: call method
- Assert: verify output
2) One logical behavior per test
Avoid giant tests that validate 20 things.
3) Use descriptive names
Bad: test1()
Good: shouldThrowExceptionWhenUserNotFound()
4) Prefer parameterized tests for rule matrices
You’ll reduce duplicates dramatically.
5) Keep tests independent
No test should depend on execution order.
Common Pitfalls (and Fixes)
- Tests not running: Ensure Gradle uses
useJUnitPlatform()and Maven Surefire is updated. - @BeforeAll not static: either make it static or add
@TestInstance(PER_CLASS). - Flaky tests: remove shared mutable state, avoid real time sleeps, isolate random seeds.
JUnit 5 FAQ
Is JUnit 5 backward compatible with JUnit 4?
Yes, via JUnit Vintage, but prefer migrating to Jupiter for new code.
What is the difference between JUnit Jupiter and JUnit Platform?
Platform runs tests; Jupiter provides annotations + APIs for writing tests.
Should I use Mockito with JUnit 5?
Yes, commonly. Mockito has a JUnit 5 integration via extensions.
What is the best way to name unit tests?
Use behavior-driven names: shouldDoXWhenY().
Conclusion
JUnit 5 makes Java testing cleaner with modern annotations, better assertions, and first-class parameterized testing. If you adopt good naming + structure (nested tests, tags, AAA), your test suite becomes readable documentation.