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
✦ Definition~90s read
What is JUnit 5 Annotations?
JUnit 5 is the de facto standard for unit testing in Java, replacing JUnit 4 with a modular architecture built on the JUnit Platform, Jupiter engine, and Vintage compatibility layer. It exists because modern Java testing demands more than simple assertions—you need lifecycle hooks, parameterized inputs, conditional execution, and extension points for mocking frameworks like Mockito or test containers.
★
JUnit 5 annotations are the vocabulary you use to tell the test runner what to do and when.
JUnit 5 annotations like @BeforeEach, @ParameterizedTest, and @TestMethodOrder give you fine-grained control over test setup, data-driven testing, and execution ordering without boilerplate. If you're working with Spring Boot, Quarkus, or any JVM-based project, JUnit 5 is the default; alternatives like TestNG still exist but lack the same ecosystem adoption and native IDE support.
Use JUnit 5 when you need reliable, composable tests that integrate seamlessly with CI pipelines—avoid it only if you're stuck on a legacy Java 7 codebase or need parallel execution semantics that TestNG handles natively.
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.
Why @BeforeEach Is Your Setup Safety Net
JUnit 5's @BeforeEach annotation marks a method to run before every test method in the class. It's the declarative replacement for setUp() in JUnit 4 — but with a critical difference: each @BeforeEach method runs in the same test instance, so state leaks between tests unless you reset it. The lifecycle is: @BeforeEach methods (inherited and declared, in order) → test method → @AfterEach methods. This runs per test, not per class, giving you O(n) setup cost for n tests. In practice, @BeforeEach is where you initialize mocks, open database connections, or set up test data. It guarantees a fresh baseline for every test, but only if you keep it fast — a 100ms setup times 1000 tests is 100 seconds. Use it for state that must be clean per test, not for expensive resources that can be shared via @BeforeAll.
Inheritance Order Matters
@BeforeEach methods from parent classes run before child class methods. If you override a parent's setup, the parent's still runs — you can't skip it.
Production Insight
Teams using shared test databases often put a transaction rollback in @BeforeEach, forgetting that @AfterEach must also roll back. Symptom: tests pass in isolation but fail in bulk because the second test sees stale data from the first. Rule: if you reset state in @BeforeEach, always pair it with a symmetric cleanup in @AfterEach.
Key Takeaway
1. @BeforeEach runs before every test — use it for per-test state, not for one-time setup.
2. Inherited @BeforeEach methods stack; you cannot override them away.
3. Keep @BeforeEach fast — it multiplies by test count and becomes your CI bottleneck.
thecodeforge.io
JUnit 5 @BeforeEach Setup Flow
Junit5 Annotations
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.
Maven Dependencies: Stop Copy-Pasting the Wrong Version
You’d think adding JUnit 5 to a project would be brainless. Yet every week some junior tags me on a PR with a NoClassDefFoundError because they grabbed junit-vintage-engine when they needed jupiter, or worse, they pinned an ancient 5.0.0 release from a 2016 blog post. Here’s the only dependency you need for modern JUnit 5 tests.
The JUnit team split the monolith into three sub-projects: Platform, Jupiter, and Vintage. For new tests, you want the junit-jupiter aggregator artifact. It pulls in the engine, the API, and the parameterized-test extension in one shot. No, you don’t need the vintage engine unless you’re running JUnit 4 tests alongside — and if you’re starting fresh, you’re not.
Don’t put this in your main source tree. This is test scope, period. And pin a specific version — 5.10.2 as of this writing — because the team releases quarterly and breaking changes in parameterized resolvers have bitten me twice.
No output. This goes in your pom.xml under <dependencies>.
Version Trap:
Never mix JUnit 4 and 5 dependencies in the same module without the vintage engine. The test runner will fail silently and skip all your tests. If you see 'No tests found with test runner', check your classpath.
Key Takeaway
Use junit-jupiter aggregator artifact for all new JUnit 5 projects; pin the version, set scope to test, and never look back.
Architecture: Why JUnit 5 Isn’t Just a Library Anymore
JUnit 5 isn’t a monolithic JAR like its predecessor. It’s three distinct sub-projects that work together like a well-oiled CI pipeline. Understanding this architecture saves you from the dreaded 'What the hell is a TestEngine?' moment when your IDE refuses to run a test.
JUnit Platform is the launcher. It sits between your build tool (Maven, Gradle, your own CLI) and the actual test engines. It discovers tests via the ServiceLoader mechanism and delegates execution. No platform, no test run.
JUnit Jupiter is the programming model you actually write code against. New annotations (@Test, @ParameterizedTest, @Tag), extension APIs, and assertion methods. This is the module you import.
JUnit Vintage exists for a single reason: backward compatibility. If you have existing JUnit 4 tests you’re too scared to rewrite, vintage bridges them to the platform. But don’t use it as a crutch. New code in 2024 should be Jupiter-only.
The practical takeaway: When you add junit-jupiter to your POM, you implicitly pull in both the platform and jupiter engines. If you ever need to debug a test runner failure, look at the TestEngine classes on your classpath first.
Run this snippet whenever your IDE shows 'No tests found.' It prints every registered engine. If jupiter is missing, you’ve got a dependency-resolution bug.
Key Takeaway
JUnit 5 is a three-part architecture: Platform launches, Jupiter writes, Vintage backfills. Know which engine is in play before debugging test failures.
Assumptions: Fail Fast or Skip Cleanly
Assumptions are the gatekeepers you didn't know you needed. They let you abort a test gracefully when conditions aren't met, rather than throwing a hard failure. Think of them as runtime preflight checks: if the database isn't accessible, skip the test. If the JVM version is too old, don't bother. This keeps your pipeline green when the environment is off, without lying about test results.
Use assumeTrue() or assumeFalse() at the top of a test. If the condition fails, JUnit marks the test as skipped, not failed. This is crucial for conditional logic in CI, where not every node has every service running. Stop using @Disabled everywhere. That's manual. Assumptions are automatic, maintainable, and honest. Your team will thank you when the build doesn't randomly burn down.
AssumptionsTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — java tutorialimport org.junit.jupiter.api.Test;
importstatic org.junit.jupiter.api.Assumptions.assumeTrue;
classAssumptionsTest {
@TestvoidtestDatabaseConnection() {
assumeTrue("true".equals(System.getenv("DB_AVAILABLE")),
"Skipping: database not available");
// actual test logic hereSystem.out.println("Database test ran.");
}
}
Output
Test skipped if DB_AVAILABLE is not set to true.
Console output (if condition passes): Database test ran.
Production Trap:
Don't use assumptions to hide broken tests — that's abuse. Use them for genuine environmental preconditions. They are not a substitute for proper mocking.
Key Takeaway
If a test shouldn't fail but can't run, skip it with assumeTrue() — never hide a real bug.
Overview: JUnit 5 Is Built for Chaos
You didn't ask for another testing framework. You asked for a tool that doesn't get in the way. JUnit 5 is that tool — modular, extensible, and finally separated from the bloated monolith that was JUnit 4. The core insight: JUnit 5 is three independent modules (JUnit Platform, Jupiter, Vintage) that each own one job. This architecture means you get lambda support, better assertions, and extension points that don't require black magic.
Stop thinking about JUnit 5 as a library. It's a runtime for writing tests that survive refactors, changing environments, and your junior dev's overuse of @Test. The payoff: tests that fail for the right reasons, not because you wrote them wrong. If you're still clinging to JUnit 4 in a greenfield project, you're wasting time. Upgrade. Now.
SimpleJUnit5Test.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — java tutorialimport org.junit.jupiter.api.Test;
importstatic org.junit.jupiter.api.Assertions.assertEquals;
classSimpleJUnit5Test {
@TestvoidverifySum() {
int result = 2 + 2;
assertEquals(4, result, "Addition failed — check your math");
}
}
Output
Test passes. Nothing fancy. That's the point.
Senior Shortcut:
When migrating from JUnit 4, use the Vintage engine to run old tests side-by-side. Migrate batch by batch. No all-nighter required.
Key Takeaway
JUnit 5 is modular by design — use the Platform to launch tests and Jupiter to write them. Don't mix versions.
@ExtendWith: Glue Your Test to Production Infrastructure
Integration tests often need Spring context, database connections, or mocks. Instead of wiring everything manually, @ExtendWith connects JUnit 5 to third-party extensions that set up and tear down infrastructure automatically. This annotation registers one or more Extension classes that JUnit calls during test lifecycle hooks. For example, SpringExtension loads the application context, MockitoExtension initializes mocks before each test, and TempDirectory creates temporary folders for file I/O tests. Why use @ExtendWith? Because it decouples test logic from setup boilerplate. Your test stays focused on assertions while the extension handles environment provisioning. No more static @BeforeAll methods that leak state between tests. Extensions are composable: you can stack multiple @ExtendWith annotations or combine them with custom extensions for logging, timeouts, or database cleanup. The pattern replaces inheritance-based test setup with pluggable, reusable components.
Test passes — MockitoExtension provides mocks without manual initialization.
Production Trap:
Extensions run per-test by default. Calling @ExtendWith(SpringExtension.class) on every test class reloads the context — use @SpringBootTest for shared context caching.
Key Takeaway
@ExtendWith replaces @BeforeAll setup infrastructure; compose extensions instead of inheriting test bases.
@TestTemplate: Run One Test Against Multiple Injection Points
Standard parameterized tests give you different arguments. @TestTemplate goes further: it runs the same test method logic against multiple contexts injected by a custom TestTemplateInvocationContextProvider. Think of it as a factory that provides not just values, but entire execution environments — including display names, extensions, and parameter resolvers. This is critical when testing the same algorithm against different data sources (file, database, stream) or different transaction managers. Why not @ParameterizedTest? Because parameterized tests assume consistent infrastructure across runs. @TestTemplate lets each invocation bring its own extension set, enabling scenarios like testing error handling with a broken database connection side by side with a happy path. The provider exposes how many invocations exist and customizes what each invocation sees, including unique display names for reporting.
TemplateTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — java tutorialimport org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.*;
classRepositoryTest {
@TestTemplate
@ExtendWith(RepositoryProvider.class)
voidshouldSaveEntity(Repository repo) {
repo.save(newEntity());
// Runs once for InMemoryRepoTest and once for PostgresRepoTest
}
}
Output
Two test executions appear in reports: 'InMemoryRepoTest' and 'PostgresRepoTest'.
Production Trap:
Each template invocation gets its own lifecycle. @BeforeEach runs per-invocation, but static setup runs once across all — watch for shared state leaks between environments.
Key Takeaway
@TestTemplate runs a test method once per invocation context, each with its own extensions and environment.
● 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.