Senior 3 min · March 30, 2026

JUnit 5 @BeforeEach — 3-Minute CI Pipeline from Setup

A @BeforeEach creating EmbeddedPostgres per test ballooned a 45-second suite to 3 minutes on CI.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • JUnit 5 annotations declare setup, teardown, and test methods
  • @BeforeEach runs before every test; @BeforeAll runs once per class
  • @ParameterizedTest drives a single test with multiple data sets
  • @DisplayName replaces cryptic method names in test reports
  • @Nested groups tests with shared state inside the same class
  • Most production failures come from misusing @BeforeAll vs @BeforeEach
Plain-English First

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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
Production Insight
CI time skyrockets when expensive setup is inside @BeforeEach — a single DB connection per test instead of once per class.
Use @BeforeAll for static resources, @BeforeEach for per-test state.
Rule: if setup takes more than 100ms, it belongs in @BeforeAll.
Key Takeaway
Expensive shared resources always go to @BeforeAll.
Fresh per-test state always goes to @BeforeEach.
The lifetime determines the annotation.
Choosing Between @BeforeEach and @BeforeAll
IfResource is immutable and expensive to create (DB, HTTP client, file handler)
UseUse @BeforeAll or a static factory method
IfState must be completely fresh per test (no shared mutation)
UseUse @BeforeEach to create new instance
IfYou need the same configuration for all tests but with optional variation
UseUse @BeforeAll with a configuration object and override in individual tests

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 ...
Production Insight
Parameterized tests catch edge cases that manual copy-paste tests miss — but only if you use meaningful names.
Without @ParameterizedTest(name = "...{0}..."), failures become 'test[3]' with no context.
Rule: always set a descriptive name that shows which parameter failed.
Key Takeaway
One test, many inputs — that's the promise.
Use @CsvSource for simple combos, @MethodSource for complex objects.
Name each iteration so failures are self-documenting.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
Production Insight
@Disabled without a reason is a landmine — nobody knows why it's skipped, and it stays disabled forever.
Use @Timeout aggressively for tests that make external calls: a 2-second timeout prevents CI stalls.
@Nested tests improve readability but watch out: each nested class adds its own @BeforeEach.
Key Takeaway
Always provide a reason for @Disabled.
Always set @Timeout on flaky or async tests.
@Nested is your friend for grouping, but don't over-nest.

Controlling Test Execution Order with @TestMethodOrder

By default, JUnit 5 does not guarantee test execution order. If your tests depend on a specific order (they shouldn't, but sometimes legacy code requires it), use @TestMethodOrder. Available strategies: MethodName (alphabetical), OrderAnnotation (custom @Order), Random (to detect flaky dependencies), or a custom implementation.

I've seen teams rely on order to share state between tests — that's a design smell. But if you're migrating a JUnit 4 suite that used @FixMethodOrder, @TestMethodOrder(MethodName) is the direct replacement. For new code, stick with @Order if you must have ordering, and never share state between tests.

Using @Order(n) is the simplest way to make order explicit — just annotate each test with @Order(1), @Order(2), etc.

OrderedTestSuite.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package io.thecodeforge.order;

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

@TestMethodOrder(MethodName.class)
class OrderedTestSuite {

    private static int counter = 0;

    @Test
    void testB() {
        assertEquals(1, ++counter);  // Runs first: testA -> testB -> testC
    }

    @Test
    void testA() {
        assertEquals(0, ++counter);  // Actually runs before testB alphabetically
    }

    @Test
    void testC() {
        // some test
    }
}

// Alternative with @Order annotation
@TestMethodOrder(OrderAnnotation.class)
class ExplicitOrderTest {

    private static int counter = 0;

    @Test
    @Order(1)
    void first() {
        assertEquals(1, ++counter);
    }

    @Test
    @Order(2)
    void second() {
        assertEquals(2, ++counter);
    }

    @Test
    @Order(3)
    void third() {
        assertEquals(3, ++counter);
    }
}
Output
# If using MethodName: testA, testB, testC (alphabetical)
# If using OrderAnnotation: first, second, third (by @Order value)
Note: Tests should be independent — this is only for migration or rare cases.
Avoid Order-Dependent Tests
Shared mutable state between tests is the #1 cause of flaky CI builds. If you need ordering because tests share data, refactor to use per-test setup instead.
Production Insight
Order-dependent tests break the moment you parallelize them.
Use @TestMethodOrder(Random) to discover hidden inter-test dependencies during CI.
If a test fails only when run in the full suite, you have a shared state bug, not an ordering problem.
Key Takeaway
Don't rely on test order.
If you must, use @Order and keep tests stateless.
Better: refactor to eliminate dependencies.

Repeated Tests and Tagging with @RepeatedTest and @Tag

@RepeatedTest runs the same test method multiple times — useful for stress testing or verifying idempotent behavior. You can customize the display name to include the current repetition: @RepeatedTest(value = 10, name = "Run {currentRepetition} of {totalRepetitions}").

@Tag marks tests for filtering. Use it to separate unit from integration tests: @Tag("fast"), @Tag("slow"). Then run mvn test -Dgroups="fast" to skip slow tests during development.

A common pattern: tag all tests that call external services as @Tag("integration") and exclude them from local builds.

RetryServiceTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package io.thecodeforge.retry;

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

class RetryServiceTest {

    @RepeatedTest(value = 5, name = "Attempt {currentRepetition} of {totalRepetitions}")
    @Tag("fast")
    @DisplayName("Idempotent retry should always succeed")
    void testIdempotentRetry(RepetitionInfo info) {
        int attempt = info.getCurrentRepetition();
        assertTrue(attempt >= 1 && attempt <= 5);
    }

    @Test
    @Tag("slow")
    @DisplayName("Payment gateway call times out after 3 retries")
    void testGatewayRetry() {
        // Simulate external call
        assertTrue(true);
    }
}

// In build.gradle or pom.xml you can filter:
// mvn test -Dgroups="fast"  (excludes @Tag("slow") tests)
Output
RetryServiceTest > Attempt 1 of 5 PASSED
RetryServiceTest > Attempt 2 of 5 PASSED
... (5 times)
RetryServiceTest > testGatewayRetry SKIPPED (when running with -Dgroups=fast)
Production Insight
@RepeatedTest is great for catching flaky failures but can slow down CI if overused — limit to 10 repetitions max.
Tag-based filtering is a must for large projects: run only fast tests during commit checks, leave integration tests for nightly builds.
Without tags, developers end up running all tests and waiting minutes.
Key Takeaway
Use @Tag to separate fast from slow tests.
Use @RepeatedTest sparingly for non-deterministic bugs.
Tag-based filtering keeps commit times under 30 seconds.
● Production incidentPOST-MORTEMseverity: high

45-Second Test Suite from a Misplaced @BeforeEach

Symptom
CI pipeline for a payment service takes over 3 minutes for 200 tests, with most time spent on test setup. Developers start skipping tests locally.
Assumption
The junior dev assumed @BeforeEach was the correct place for all setup because 'each test needs a clean database'. They didn't realize an embedded database connection can be shared.
Root cause
The @BeforeEach method created a new EmbeddedPostgres instance and DataSource for every test, including expensive initialization overhead. The test also closed and reopened the connection in @AfterEach.
Fix
Moved the EmbeddedPostgres creation and DataSource initialization to @BeforeAll (static method), kept only the PaymentService instantiation (cheap) in @BeforeEach. Reduced test suite time from 45s to under 4s.
Key lesson
  • Use @BeforeAll for any resource that can be shared across all tests without shared mutable state
  • Use @BeforeEach only for per-test state that must be fresh (e.g., instance of class under test)
  • When in doubt, measure the cost — add System.currentTimeMillis() in @BeforeEach to see how long setup takes
Production debug guideIdentify and fix common annotation-related issues in CI or local runs4 entries
Symptom · 01
@BeforeAll method is not static
Fix
Check if the method is static. If your class uses @TestInstance(Lifecycle.PER_CLASS) you can make it non-static. Otherwise add static keyword.
Symptom · 02
@DisplayName is ignored in IDE test runner
Fix
Some runners (e.g., IntelliJ older versions) may ignore @DisplayName from nested classes. Ensure you're using the latest IntelliJ or run via Maven/Gradle.
Symptom · 03
@ParameterizedTest prints generic 'test[0]', 'test[1]' in report
Fix
Add a name attribute: @ParameterizedTest(name = "{index}: {arguments}") to make each run human-readable. Use {0}, {1}, etc. for parameter placeholders.
Symptom · 04
@Disabled reason not shown in report
Fix
Check if the reason string is empty or contains only whitespace. Always provide a reason string: @Disabled('JIRA-1234: DB migration pending')
★ JUnit 5 Annotation Issues — Quick FixesWhen a test fails unexpectedly, check these annotation-related pitfalls first.
Test runs but @BeforeEach never executes
Immediate action
Check that @BeforeEach is on a method (not a constructor). JUnit 5 does not support constructor injection by default.
Commands
grep -rn '@BeforeEach' src/test/java/
grep -rn '@Test' src/test/java/ | wc -l
Fix now
Add @BeforeEach on a void method with no parameters, placed in the same class as the @Test methods.
@BeforeAll executes but takes seconds for a single test+
Immediate action
Measure the @BeforeAll method duration. If it's slow, check if you're creating a new resource (e.g., DB) every time.
Commands
java -jar junit-platform-console-standalone-1.10.0.jar --scan-class-path --details verbose
grep -rn '@BeforeAll' src/test/java/
Fix now
Move expensive resource creation to @BeforeAll but cache the result in a static field. If that resource must be fresh per test, consider test fixtures instead.
Parameterized test throws 'No ParameterResolver registered for parameter'+
Immediate action
Check that the parameter source (e.g., @CsvSource) is correctly annotating the method. The parameter type must match the source.
Commands
javap -p -c TargetTestClass.class 2>/dev/null | grep 'ParameterizedTest'
mvn test -Dtest=TargetTestClass -pl .
Fix now
Ensure the method has exactly the right number of parameters for the data source. For @CsvSource with two columns, the method must take two parameters.
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=)
@TestMethodOrderControls test execution orderNo@FixMethodOrder
@RepeatedTestRepeats test N timesNoNo direct equivalent
@TagLabels for filteringNo@Category

Key takeaways

1
@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.
2
@ParameterizedTest with @CsvSource or @MethodSource eliminates repetitive test methods for multiple input variations.
3
@Nested classes group related tests with their own @BeforeEach setup, making complex test classes readable.
4
@DisplayName on every test and class transforms opaque method names into human-readable test documentation in CI reports.
5
@Timeout is essential for tests that involve async code or network calls
catches accidental blocking that would otherwise stall your CI pipeline indefinitely.
6
@TestMethodOrder with Random strategy helps uncover hidden test dependencies that break under parallelism.
7
@Tag separates fast from slow tests, keeping commit builds under 30 seconds while integration tests run nightly.

Common mistakes to avoid

5 patterns
×

Using @BeforeEach for expensive shared setup

Symptom
Test suite runs 10x slower than expected; each test creates new DB connections or starts servers.
Fix
Move all expensive, shareable setup to @BeforeAll. Keep only cheap per-test instantiation in @BeforeEach.
×

@BeforeAll method not static

Symptom
JUnit 5 fails with 'Only static methods can be annotated with @BeforeAll' error.
Fix
Add 'static' modifier to @BeforeAll methods. Alternatively annotate the class with @TestInstance(TestInstance.Lifecycle.PER_CLASS) to allow non-static lifecycle methods.
×

Multiple @Test methods that depend on each other's side effects

Symptom
Tests pass individually but fail when run together in the suite. Order-dependent failures.
Fix
Make each test independent. Use @BeforeEach to reset shared state. If ordering is absolutely required, use @TestMethodOrder and @Order, but avoid sharing mutable state.
×

Not using @DisplayName

Symptom
CI reports show method names like 'test3', 'testMethod', which provide no context about what failed.
Fix
Add @DisplayName("short description") to every test. This transforms reports into documentation.
×

Using @ParameterizedTest without a custom name template

Symptom
Test runner displays generic 'test[0]', 'test[1]' — impossible to tell which input failed without reading the output.
Fix
Add name attribute: @ParameterizedTest(name = "{index}: {arguments}") or include specific parameter placeholders like {0}, {1}.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between @BeforeEach and @BeforeAll, and when woul...
Q02SENIOR
How do you write a parameterized test in JUnit 5?
Q03SENIOR
A test suite takes 3 minutes but only has 50 tests. What JUnit lifecycle...
Q04SENIOR
How do you group related tests together with shared setup in JUnit 5?
Q05SENIOR
What is the purpose of @TestMethodOrder and when would you use it?
Q01 of 05JUNIOR

What is the difference between @BeforeEach and @BeforeAll, and when would you use each?

ANSWER
@BeforeEach runs before every test method — use for cheap, per-test state (e.g., instantiate service class, reset mocks). @BeforeAll runs once per test class — use for expensive shared setup (e.g., start embedded database, load config). @BeforeAll must be static unless @TestInstance(PER_CLASS) is used. Example: for a test suite with 200 payment tests, creating an EmbeddedPostgres in @BeforeAll saved 40 seconds compared to @BeforeEach.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is the difference between @BeforeEach and @BeforeAll in JUnit 5?
02
How do I skip a test in JUnit 5?
03
How do I run the same test with multiple inputs in JUnit 5?
04
How do I group related tests in JUnit 5?
05
How do I control the order of tests in JUnit 5?
06
What is the difference between @RepeatedTest and @ParameterizedTest?
🔥

That's Advanced Java. Mark it forged?

3 min read · try the examples if you haven't

Previous
Java Agent and Instrumentation
26 / 28 · Advanced Java
Next
Mockito verify(): How to Assert Method Calls in Unit Tests