Homeβ€Ί Javaβ€Ί JUnit 5 Annotations: @Test, @BeforeEach, @AfterEach and More

JUnit 5 Annotations: @Test, @BeforeEach, @AfterEach and More

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: Advanced Java β†’ Topic 24 of 26
Master JUnit 5 annotations including @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, @Disabled, @ParameterizedTest and @DisplayName with real Java examples using io.
πŸ§‘β€πŸ’» Beginner-friendly β€” no prior Java experience needed
In this tutorial, you'll learn:
  • @BeforeEach runs before every test β€” use it for cheap per-test setup. @BeforeAll runs once β€” use it for expensive shared setup like database or network clients.
  • @ParameterizedTest with @CsvSource or @MethodSource eliminates repetitive test methods for multiple input variations.
  • @Nested classes group related tests with their own @BeforeEach setup, making complex test classes readable.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
JUnit 5 annotations are the vocabulary you use to tell the test runner what to do and when. @Test marks a method as a test. @BeforeEach runs setup code before every test. @AfterEach runs cleanup after every test. Once you know what each annotation means, writing structured, readable test suites becomes natural.

JUnit 5 shipped in 2017 and most Java teams are still not using it to its full potential β€” they write JUnit 4 tests with JUnit 5 imports. The annotation model is genuinely better: display names, parameterized tests, nested test classes, and lifecycle extensions are all first-class. Understanding the lifecycle annotations specifically is what separates 'tests that happen to pass' from a test suite you can trust.

I've reviewed hundreds of test suites and the single most common issue is misuse of @BeforeAll vs @BeforeEach β€” specifically using @BeforeEach to set up shared expensive state (database connections, HTTP clients) that should be @BeforeAll. On a 200-test suite this is the difference between a 3-second run and a 45-second run.

Core Lifecycle Annotations

JUnit 5's lifecycle annotations control the setup and teardown flow around your tests. Getting this right is the foundation of a fast, reliable test suite.

@BeforeEach runs before every single test method. Use it for state that must be fresh per test β€” creating a new instance of the class under test, resetting mocks, clearing an in-memory list. If two tests share state through a field set in @BeforeEach, they're still isolated.

@AfterEach runs after every test regardless of pass or fail. Use it to release resources acquired in @BeforeEach β€” close a file handle, clear a database row created during the test.

@BeforeAll runs once before all tests in the class. Must be static (unless @TestInstance(PER_CLASS) is used). Use it for expensive shared setup: starting an embedded database, loading a large test fixture, initialising a test HTTP client. Creating these in @BeforeEach means rebuilding them for every test β€” that's the bug I see most.

@AfterAll runs once after all tests. Tear down @BeforeAll resources here.

PaymentServiceTest.java Β· JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
package io.thecodeforge.payment;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class PaymentServiceTest {

    // Shared across ALL tests in this class β€” initialised once
    private static EmbeddedPostgres embeddedDb;
    private static DataSource dataSource;

    // Fresh instance per test β€” guarantees test isolation
    private PaymentService paymentService;

    @BeforeAll
    static void setUpDatabase() {
        // Expensive β€” do this ONCE, not before every test
        embeddedDb = EmbeddedPostgres.start();
        dataSource = embeddedDb.getPostgresDatabase();
        System.out.println("Embedded DB started β€” shared across all tests");
    }

    @BeforeEach
    void setUpService() {
        // Cheap β€” fresh PaymentService instance per test
        paymentService = new PaymentService(dataSource);
    }

    @Test
    @DisplayName("processPayment should return SUCCESS for valid payment request")
    void processPayment_validRequest_returnsSuccess() {
        PaymentRequest request = new PaymentRequest("customer-42", 100_00, "GBP");
        PaymentResult result = paymentService.processPayment(request);
        assertEquals(PaymentStatus.SUCCESS, result.getStatus());
    }

    @Test
    @DisplayName("processPayment should throw for null payment reference")
    void processPayment_nullReference_throwsIllegalArgument() {
        assertThrows(IllegalArgumentException.class,
            () -> paymentService.processPayment(null),
            "Expected exception for null payment request");
    }

    @AfterEach
    void cleanUpTestData() {
        // Clean only data created during THIS test
        paymentService.deleteTestPayments("customer-42");
    }

    @AfterAll
    static void tearDownDatabase() {
        embeddedDb.close();
        System.out.println("Embedded DB stopped");
    }
}
β–Ά Output
# JUnit 5 test execution order:
# 1. @BeforeAll (once)
# 2. @BeforeEach β†’ test 1 β†’ @AfterEach
# 3. @BeforeEach β†’ test 2 β†’ @AfterEach
# 4. @AfterAll (once)

Embedded DB started β€” shared across all tests

Test run: 2 tests passed

Embedded DB stopped

Parameterized Tests with @ParameterizedTest

Parameterized tests are one of JUnit 5's biggest improvements over JUnit 4. Instead of writing five near-identical test methods for five input variations, you write one and drive it with data.

@ValueSource for primitive lists. @CsvSource for multiple parameters per test case. @MethodSource for complex objects. @EnumSource for testing against all values of an enum.

I use @CsvSource for the majority of business-logic edge cases β€” it keeps the test data inline and readable without a separate data factory.

PaymentValidatorTest.java Β· JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
package io.thecodeforge.payment;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class PaymentValidatorTest {

    private final PaymentValidator validator = new PaymentValidator();

    @ParameterizedTest(name = "amount {0} should be valid")
    @ValueSource(ints = {1, 100, 999_99, 1_000_00})
    void validate_validAmounts_pass(int amountInPence) {
        assertTrue(validator.isValidAmount(amountInPence));
    }

    @ParameterizedTest(name = "amount={0} currency={1} should {2}")
    @CsvSource({
        "100,   GBP, true",
        "100,   USD, true",
        "0,     GBP, false",   // zero amount invalid
        "-100,  GBP, false",   // negative amount invalid
        "100,   XYZ, false"    // unknown currency invalid
    })
    void validate_amountAndCurrency(int amount, String currency, boolean expected) {
        assertEquals(expected, validator.isValid(amount, currency));
    }

    @ParameterizedTest
    @EnumSource(value = PaymentMethod.class, names = {"CARD", "BANK_TRANSFER"})
    void validate_supportedPaymentMethods_pass(PaymentMethod method) {
        assertTrue(validator.isSupportedMethod(method));
    }

    // @MethodSource for complex objects
    @ParameterizedTest
    @MethodSource("invalidPaymentRequests")
    void validate_invalidRequests_fail(PaymentRequest request, String reason) {
        assertFalse(validator.isValid(request), "Expected invalid: " + reason);
    }

    static Stream<Arguments> invalidPaymentRequests() {
        return Stream.of(
            Arguments.of(new PaymentRequest(null, 100, "GBP"), "null customer"),
            Arguments.of(new PaymentRequest("c1", -1, "GBP"), "negative amount"),
            Arguments.of(new PaymentRequest("c1", 100, null), "null currency")
        );
    }
}
β–Ά Output
PaymentValidatorTest > validate_validAmounts_pass(int) > amount 1 should be valid PASSED
PaymentValidatorTest > validate_validAmounts_pass(int) > amount 100 should be valid PASSED
PaymentValidatorTest > validate_amountAndCurrency > amount=100 currency=GBP should true PASSED
PaymentValidatorTest > validate_amountAndCurrency > amount=0 currency=GBP should false PASSED
# ... all 14 parameterized cases ...

Other Key Annotations

The annotations that complete the toolkit: @Disabled skips a test with an explanatory reason. @DisplayName replaces cryptic method names in test reports. @Nested groups related tests with shared setup. @Timeout fails a test that runs too long β€” essential for catching accidental blocking calls in async code.

OrderServiceTest.java Β· JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
package io.thecodeforge.order;

import org.junit.jupiter.api.*;
import java.time.Duration;

class OrderServiceTest {

    @Test
    @Disabled("Disabled until JIRA-1234 is resolved β€” payment gateway sandbox is down")
    @DisplayName("processOrder should trigger payment")
    void processOrder_triggersPayment() {
        // Will be skipped with the reason logged
    }

    @Test
    @Timeout(2)  // Fails if this test takes more than 2 seconds
    @DisplayName("fetchOrder should return within SLA")
    void fetchOrder_returnsWithinSla() {
        OrderService service = new OrderService();
        Order result = service.fetchOrder("order-123");
        assertNotNull(result);
    }

    @Nested
    @DisplayName("When order is in PENDING state")
    class WhenPending {

        private Order pendingOrder;

        @BeforeEach
        void setUp() {
            pendingOrder = Order.builder().status(OrderStatus.PENDING).build();
        }

        @Test
        @DisplayName("can be cancelled")
        void canBeCancelled() {
            assertTrue(pendingOrder.canBeCancelled());
        }

        @Test
        @DisplayName("cannot be refunded")
        void cannotBeRefunded() {
            assertFalse(pendingOrder.canBeRefunded());
        }
    }
}
β–Ά Output
@Disabled test:
OrderServiceTest > processOrder should trigger payment SKIPPED
Reason: Disabled until JIRA-1234 is resolved

@Nested test output:
OrderServiceTest > When order is in PENDING state > can be cancelled PASSED
OrderServiceTest > When order is in PENDING state > cannot be refunded PASSED
AnnotationRunsStatic?JUnit 4 Equivalent
@TestMarks a test methodNo@Test
@BeforeEachBefore every testNo@Before
@AfterEachAfter every testNo@After
@BeforeAllOnce before all testsYes (by default)@BeforeClass
@AfterAllOnce after all testsYes (by default)@AfterClass
@DisabledSkips the testNo@Ignore
@DisplayNameSets the test report labelNoNo equivalent
@ParameterizedTestRuns test multiple times with dataNoNo direct equivalent
@NestedGroups tests in a classNoNo direct equivalent
@TimeoutFails if test exceeds durationNo@Test(timeout=)

🎯 Key Takeaways

  • @BeforeEach runs before every test β€” use it for cheap per-test setup. @BeforeAll runs once β€” use it for expensive shared setup like database or network clients.
  • @ParameterizedTest with @CsvSource or @MethodSource eliminates repetitive test methods for multiple input variations.
  • @Nested classes group related tests with their own @BeforeEach setup, making complex test classes readable.
  • @DisplayName on every test and class transforms opaque method names into human-readable test documentation in CI reports.
  • @Timeout is essential for tests that involve async code or network calls β€” catches accidental blocking that would otherwise stall your CI pipeline indefinitely.

⚠ Common Mistakes to Avoid

  • βœ•Using @BeforeEach for expensive shared setup like database connections β€” this runs before every single test, not once. Use @BeforeAll for expensive resources.
  • βœ•@BeforeAll method not being static β€” JUnit 5 requires @BeforeAll methods to be static unless you annotate the class with @TestInstance(Lifecycle.PER_CLASS).
  • βœ•Writing multiple @Test methods that depend on each other's side effects β€” each @Test must be independent. JUnit does not guarantee execution order without @TestMethodOrder.
  • βœ•Not using @DisplayName β€” test reports become unreadable with method names like 'test3'. DisplayName is the difference between a test suite that communicates intent and one that doesn't.

Interview Questions on This Topic

  • QWhat is the difference between @BeforeEach and @BeforeAll, and when would you use each?
  • QHow do you write a parameterized test in JUnit 5?
  • QA test suite takes 3 minutes but only has 50 tests. What JUnit lifecycle annotation misuse would you investigate first?
  • QHow do you group related tests together with shared setup in JUnit 5?

Frequently Asked Questions

What is the difference between @BeforeEach and @BeforeAll in JUnit 5?

@BeforeEach runs before every single test method in the class β€” it's for per-test setup. @BeforeAll runs once before any tests in the class run β€” it's for expensive shared setup like starting an embedded database or creating an HTTP client. @BeforeAll methods must be static unless you use @TestInstance(PER_CLASS).

How do I skip a test in JUnit 5?

Use @Disabled on the test method or class. Always include a reason: @Disabled('Reason why this is skipped'). The reason appears in the test report. Avoid using @Disabled without a reason β€” it becomes mystery disabled tests that nobody removes.

How do I run the same test with multiple inputs in JUnit 5?

Use @ParameterizedTest with a source annotation. @ValueSource works for single primitive parameters. @CsvSource works for multiple parameters per test case. @MethodSource works for complex objects. Add a name attribute to @ParameterizedTest to make each run identifiable in reports.

πŸ”₯
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousJava Agent and InstrumentationNext β†’Mockito verify(): How to Assert Method Calls in Unit Tests
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged