A test suite that takes 20 minutes to run does not get run. Developers skip it, work around it, or disable it entirely. Speed is not a nice-to-have in automated testing — it is fundamental to whether your tests actually provide value. This guide diagnoses every common cause of slow JUnit 6 tests and provides concrete, measurable optimisations with real performance numbers.
Measuring First: Identify Your Bottlenecks
Before optimising anything, measure. You need to know which tests are slowest before you can improve them.
# Maven: run tests and capture timing in the Surefire report
mvn test
# Check: target/surefire-reports/*.xml — each test has 'time' attribute
# Find the slowest tests across all XML reports
grep -h 'time=' target/surefire-reports/*.xml |
grep -oP 'time="[0-9.]+"' |
sort -t'"' -k2 -rn |
head -20
# Gradle: generate a test report with timing
./gradlew test
# Check: build/reports/tests/test/index.html
# Sort by duration in the 'Classes' tab
Problem 1: Spring Context Starts on Every Test Class
Starting a Spring ApplicationContext takes 3–10 seconds. If 20 test classes each trigger a fresh context, that is 60–200 seconds of pure startup overhead before a single assertion runs.
// SLOW: Different @MockBean in each class = unique context = fresh startup each time
@SpringBootTest
class OrderControllerTest {
@MockBean PaymentGateway gateway; // unique context config A
}
@SpringBootTest
class InvoiceControllerTest {
@MockBean TaxCalculator taxCalc; // unique context config B = ANOTHER startup
}
// FAST: Shared context configuration = Spring caches and reuses it
// Define all @MockBeans in ONE base class — all subclasses share the same context
@SpringBootTest
@MockBeanAnnotationsFromClass // or simply put all @MockBeans here
public abstract class BaseIntegrationTest {
// Declare ALL mocks used by ANY integration test here
@MockBean protected PaymentGateway paymentGateway;
@MockBean protected TaxCalculator taxCalculator;
@MockBean protected EmailService emailService;
// Spring starts ONE context for all subclasses = startup cost paid ONCE
}
class OrderControllerTest extends BaseIntegrationTest { /* tests */ }
class InvoiceControllerTest extends BaseIntegrationTest { /* tests */ }
// Context reused — 3-10 second startup paid ONCE instead of per class

