JUnit 5 — @Test, Assertions, Assumptions, Parameterized Tests, and Test Lifecycle
In this tutorial, you will learn about JUnit 5. We cover key concepts, practical examples, and best practices to help you master this topic.
JUnit 5 is the standard testing framework for Java, providing annotations, assertions, and extension points for writing and running tests. JUnit 5 is the third major version — it is modular (JUnit Platform + JUnit Jupiter + JUnit Vintage) and supports Java 8+ features like lambdas and streams.
What You'll Learn
- JUnit 5 architecture: Platform, Jupiter, Vintage
- @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll
- Assertions: assertEquals, assertThrows, assertAll
- Assumptions: assumeTrue, assumingThat
- Parameterized tests with @ValueSource, @CsvSource, @MethodSource
Why It Matters
Automated testing is non-negotiable for professional software development. JUnit 5 is the industry standard for Java Unit Testing. Understanding parameterized tests and assertions helps you write comprehensive, readable tests.
Real-World Use
Every Spring Boot project includes spring-boot-starter-test with JUnit 5. CI/CD pipelines run JUnit tests on every commit. Code Coverage tools (JaCoCo) integrate with JUnit.
JUnit 5 Architecture
- JUnit Platform — foundation for launching tests on the JVM
- JUnit Jupiter — the programming model (annotations, assertions)
- JUnit Vintage — backward compatibility with JUnit 4
Basic Test
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void shouldAddTwoNumbers() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result);
}
}
Test Lifecycle
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void initAll() {
System.out.println("Before all tests");
}
@BeforeEach
void init() {
System.out.println("Before each test");
}
@Test
void testOne() {
System.out.println("Test one");
}
@Test
void testTwo() {
System.out.println("Test two");
}
@AfterEach
void tearDown() {
System.out.println("After each test");
}
@AfterAll
static void tearDownAll() {
System.out.println("After all tests");
}
}
Assertions
@Test
void standardAssertions() {
assertEquals(4, calculator.add(2, 2));
assertNotEquals(5, calculator.add(2, 2));
assertTrue(calculator.isPositive(5));
assertFalse(calculator.isPositive(-1));
assertNull(null);
assertNotNull("hello");
}
@Test
void groupedAssertions() {
assertAll("person",
() -> assertEquals("Alice", person.getName()),
() -> assertEquals(30, person.getAge())
);
}
@Test
void exceptionAssertion() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
calculator.divide(1, 0);
});
assertEquals("Cannot divide by zero", exception.getMessage());
}
@Test
void timeoutAssertion() {
assertTimeout(ofMillis(100), () -> {
Thread.sleep(50);
});
}
Third-Party Matchers
For richer assertions, use AssertJ or Hamcrest:
import static org.assertj.core.api.Assertions.*;
assertThat(list).hasSize(3)
.contains("Alice")
.allSatisfy(name -> assertThat(name).isNotEmpty());
Assumptions
Assumptions skip tests when conditions are not met:
@Test
void testOnDevelopmentEnvironment() {
assumeTrue("dev".equals(System.getenv("ENV")));
// Only runs in dev environment
}
@Test
void testForSpecificOS() {
assumingThat(System.getProperty("os.name").contains("Linux"), () -> {
// Only runs on Linux
assertEquals("/tmp", System.getProperty("java.io.tmpdir"));
});
}
Parameterized Tests
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testWithValueSource(int number) {
assertTrue(number > 0);
}
@ParameterizedTest
@CsvSource({
"apple, 5",
"banana, 6",
"cherry, 6"
})
void testWithCsvSource(String word, int expectedLength) {
assertEquals(expectedLength, word.length());
}
@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana", "cherry");
}
@ParameterizedTest
@EnumSource(Day.class)
void testWithEnumSource(Day day) {
assertNotNull(day);
}
Test Instance Lifecycle
By default, JUnit 5 creates a new test instance for each test method:
// Default: @TestInstance(Lifecycle.PER_METHOD)
// Each test gets a new instance
// Alternative: share instance across tests
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedStateTest {
private int count = 0;
@Test
void testOne() { count++; } // modifies shared state
@Test
void testTwo() { count++; }
}
PER_CLASS allows @BeforeAll/@AfterAll to be non-static.
Nested Tests
class StackTest {
private Stack<String> stack;
@Test
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Nested
class WhenElementsPushed {
@BeforeEach
void pushElements() {
stack.push("A");
stack.push("B");
}
@Test
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
void popReturnsLastElement() {
assertEquals("B", stack.pop());
}
}
}
Common Mistakes
- Forgetting
@Testannotation. The method is not discovered as a test. - Using
assertTrue(actual == expected)instead ofassertEquals(expected, actual). The latter provides better failure messages. - Not using
assertAll()for multiple assertions. WithoutassertAll(), the first failure stops the test, hiding subsequent failures. - Making tests depend on execution order. Tests should be independent. Use
@TestMethodOrderonly when absolutely necessary. - Testing implementation details, not behavior. Tests should verify public contracts, not private methods.
Practice Questions
1. What is the difference between JUnit 4 and JUnit 5?
JUnit 5 is modular (Platform + Jupiter + Vintage). It supports Java 8 features (lambdas, streams). Annotations changed: @Before -> @BeforeEach, @BeforeClass -> @BeforeAll.
2. What does assertAll() do?
It executes all assertions in the lambda and collects all failures, reporting them together. Without it, the first failure aborts the test.
3. What is the purpose of @ParameterizedTest?
It runs the same test method multiple times with different arguments, reducing boilerplate and improving test coverage.
4. What is the difference between assumeTrue and assertTrue?
assertTrue fails the test if the condition is false. assumeTrue aborts (skips) the test if the condition is false — useful for environment-dependent tests.
5. What is the default test instance lifecycle?
PER_METHOD — a new test instance is created for each test method.
Challenge Question:
Write a parameterized test for a StringUtils class with methods reverse(String), isPalindrome(String), and countVowels(String). Use @CsvSource and @MethodSource. Include edge cases: null, empty string, single character, mixed case, Unicode characters.
FAQ
{{< faq "How do I run a specific test method?" "Use mvn test -Dtest=ClassName#methodName (Maven) or ./gradlew test --tests \"*.ClassName.methodName\" (Gradle)." >}}
{{< faq "How do I disable a test?" "Use @Disabled (JUnit 5) or @Ignore (JUnit 4) on the test method or class. Provide a reason: @Disabled(\"TODO: fix after refactoring\")." >}}
Mini Project
Write a comprehensive test suite for a BankAccount class:
- Test account creation with initial balance
- Test deposit with positive and negative amounts
- Test withdrawal with sufficient and insufficient funds
- Test transfer between accounts
- Use parameterized tests for withdrawal limits
- Use
assertAll()to verify multiple conditions - Use nested tests (
@Nested) to group related tests - Use
@BeforeEachto create a fresh account before each test - Verify that overdraft and negative deposit throw exceptions
What's Next
JUnit tests verify behavior in isolation, but real applications have collaborators. Lesson 48 covers Mockito — the standard mocking framework for Java, with @Mock, @InjectMocks, stubbing, verification, BDDMockito, and ArgumentCaptor.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro