Skip to content

JUnit 5 — @Test, Assertions, Assumptions, Parameterized Tests, and Test Lifecycle

DodaTech Updated 2026-06-28 5 min read

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

  1. Forgetting @Test annotation. The method is not discovered as a test.
  2. Using assertTrue(actual == expected) instead of assertEquals(expected, actual). The latter provides better failure messages.
  3. Not using assertAll() for multiple assertions. Without assertAll(), the first failure stops the test, hiding subsequent failures.
  4. Making tests depend on execution order. Tests should be independent. Use @TestMethodOrder only when absolutely necessary.
  5. 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)." >}}

What is the difference between `@BeforeEach` and `@BeforeAll`?

@BeforeEach runs before each test method. @BeforeAll runs once before all test methods in the class. @BeforeAll methods must be static (unless using PER_CLASS lifecycle).

Can I use JUnit 5 with Maven?

Yes. Add junit-jupiter dependency and the maven-surefire-plugin (which supports JUnit 5 since version 2.22.0).

What is a `TestTemplate`?

A generic template for test cases. The best-known implementation is @RepeatedTest — it runs the same test multiple times.

{{< 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:

  1. Test account creation with initial balance
  2. Test deposit with positive and negative amounts
  3. Test withdrawal with sufficient and insufficient funds
  4. Test transfer between accounts
  5. Use parameterized tests for withdrawal limits
  6. Use assertAll() to verify multiple conditions
  7. Use nested tests (@Nested) to group related tests
  8. Use @BeforeEach to create a fresh account before each test
  9. 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