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.*;
importstatic org.junit.jupiter.api.Assertions.*;
classPaymentServiceTest {
// Shared across ALL tests in this class — initialised onceprivatestaticEmbeddedPostgres embeddedDb;
privatestaticDataSource dataSource;
// Fresh instance per test — guarantees test isolationprivatePaymentService paymentService;
@BeforeAllstaticvoidsetUpDatabase() {
// Expensive — do this ONCE, not before every test
embeddedDb = EmbeddedPostgres.start();
dataSource = embeddedDb.getPostgresDatabase();
System.out.println("Embedded DB started — shared across all tests");
}
@BeforeEachvoidsetUpService() {
// Cheap — fresh PaymentService instance per test
paymentService = newPaymentService(dataSource);
}
@Test
@DisplayName("processPayment should return SUCCESS for valid payment request")
voidprocessPayment_validRequest_returnsSuccess() {
PaymentRequest request = newPaymentRequest("customer-42", 100_00, "GBP");
PaymentResult result = paymentService.processPayment(request);
assertEquals(PaymentStatus.SUCCESS, result.getStatus());
}
@Test
@DisplayName("processPayment should throw for null payment reference")
voidprocessPayment_nullReference_throwsIllegalArgument() {
assertThrows(IllegalArgumentException.class,
() -> paymentService.processPayment(null),
"Expected exception for null payment request");
}
@AfterEachvoidcleanUpTestData() {
// Clean only data created during THIS test
paymentService.deleteTestPayments("customer-42");
}
@AfterAllstaticvoidtearDownDatabase() {
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 > 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;
classOrderServiceTest {
@Test
@Disabled("Disabled until JIRA-1234 is resolved — payment gateway sandbox is down")
@DisplayName("processOrder should trigger payment")
voidprocessOrder_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")
voidfetchOrder_returnsWithinSla() {
OrderService service = newOrderService();
Order result = service.fetchOrder("order-123");
assertNotNull(result);
}
@Nested
@DisplayName("When order is in PENDING state")
classWhenPending {
privateOrder pendingOrder;
@BeforeEachvoidsetUp() {
pendingOrder = Order.builder().status(OrderStatus.PENDING).build();
}
@Test
@DisplayName("can be cancelled")
voidcanBeCancelled() {
assertTrue(pendingOrder.canBeCancelled());
}
@Test
@DisplayName("cannot be refunded")
voidcannotBeRefunded() {
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.
# 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.*;
importstatic org.junit.jupiter.api.Assertions.*;
classRetryServiceTest {
@RepeatedTest(value = 5, name = "Attempt {currentRepetition} of {totalRepetitions}")
@Tag("fast")
@DisplayName("Idempotent retry should always succeed")
voidtestIdempotentRetry(RepetitionInfo info) {
int attempt = info.getCurrentRepetition();
assertTrue(attempt >= 1 && attempt <= 5);
}
@Test
@Tag("slow")
@DisplayName("Payment gateway call times out after 3 retries")
voidtestGatewayRetry() {
// Simulate external callassertTrue(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.
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.
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.
Annotation
Runs
Static?
JUnit 4 Equivalent
@Test
Marks a test method
No
@Test
@BeforeEach
Before every test
No
@Before
@AfterEach
After every test
No
@After
@BeforeAll
Once before all tests
Yes (by default)
@BeforeClass
@AfterAll
Once after all tests
Yes (by default)
@AfterClass
@Disabled
Skips the test
No
@Ignore
@DisplayName
Sets the test report label
No
No equivalent
@ParameterizedTest
Runs test multiple times with data
No
No direct equivalent
@Nested
Groups tests in a class
No
No direct equivalent
@Timeout
Fails if test exceeds duration
No
@Test(timeout=)
@TestMethodOrder
Controls test execution order
No
@FixMethodOrder
@RepeatedTest
Repeats test N times
No
No direct equivalent
@Tag
Labels for filtering
No
@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.
Q02 of 05SENIOR
How do you write a parameterized test in JUnit 5?
ANSWER
Annotate the test method with @ParameterizedTest and provide a data source. For simple values use @ValueSource({1,2,3}). For multiple parameters use @CsvSource({"val1,val2","val3,val4"}). For complex objects use @MethodSource("factoryMethod") where the factory returns a Stream of Arguments. Always set a custom name to identify each run: @ParameterizedTest(name = "{index}: {arguments}").
Q03 of 05SENIOR
A test suite takes 3 minutes but only has 50 tests. What JUnit lifecycle annotation misuse would you investigate first?
ANSWER
I'd look for @BeforeEach methods that create expensive resources like database connections, HTTP clients, or large data structures. The default assumption is likely that each test needs a fresh resource, but often those resources can be shared across all tests via @BeforeAll. I'd add timing to each @BeforeEach to confirm. In one production case, moving a DataSource creation from @BeforeEach to @BeforeAll cut the suite from 3 minutes to 12 seconds.
Q04 of 05SENIOR
How do you group related tests together with shared setup in JUnit 5?
ANSWER
Use @Nested classes. Inside the outer test class, define inner classes annotated with @Nested. Each inner class can have its own @BeforeEach, @Test methods, and fields. The outer class setup (@BeforeAll, @BeforeEach) runs before the inner's setup. This organizes tests by scenario (e.g., @DisplayName("When payment is declined")). It improves readability and allows shared variables within the nested scope.
Q05 of 05SENIOR
What is the purpose of @TestMethodOrder and when would you use it?
ANSWER
@TestMethodOrder defines the order JUnit 5 runs test methods in a class. Use MethodName for alphabetical, OrderAnnotation with @Order integers, or Random to detect flaky dependencies. Use it sparingly — tests should be independent. I've only needed it when migrating legacy JUnit 4 tests that relied on @FixMethodOrder, or when using Random to surface shared state bugs.
01
What is the difference between @BeforeEach and @BeforeAll, and when would you use each?
JUNIOR
02
How do you write a parameterized test in JUnit 5?
SENIOR
03
A test suite takes 3 minutes but only has 50 tests. What JUnit lifecycle annotation misuse would you investigate first?
SENIOR
04
How do you group related tests together with shared setup in JUnit 5?
SENIOR
05
What is the purpose of @TestMethodOrder and when would you use it?
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
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).
Was this helpful?
02
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.
Was this helpful?
03
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.
Was this helpful?
04
How do I group related tests in JUnit 5?
Use @Nested inner classes. Each nested class can have its own @BeforeEach, @AfterEach, and @Test methods. The display name of the nested class appears in the test report: Outer > When condition > test().
Was this helpful?
05
How do I control the order of tests in JUnit 5?
Use @TestMethodOrder on the test class with a strategy: MethodName (alphabetical), OrderAnnotation (custom @Order), Random (to detect dependencies). For explicit order, annotate each test with @Order(n).
Was this helpful?
06
What is the difference between @RepeatedTest and @ParameterizedTest?
@RepeatedTest runs the same test method N times with the same configuration — useful for stress testing. @ParameterizedTest runs the test once per input data set — each run can use different parameters.