Two capabilities that TestNG made easy — test timeouts and parallel execution — are fully supported in JUnit 5 but configured differently. Timeouts live on annotations or in assertions; parallelism lives in a properties file. The key concept for both is thread safety: once tests run concurrently, shared mutable state becomes a bug waiting to happen. This lesson covers the mechanics and the guard you need: @ResourceLock.
@Timeout — enforcing a time budget
@Timeout on a test method declares a maximum duration. If the test exceeds it, JUnit fails the test with a TimeoutException:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import java.util.concurrent.TimeUnit;
class ApiTimeoutTest {
@Test
@Timeout(value = 5, unit = TimeUnit.SECONDS)
void shouldCompleteWithin5Seconds() {
Response response = apiClient.getUsers();
assertEquals(200, response.getStatusCode());
}
}The default unit is TimeUnit.SECONDS, so @Timeout(5) is equivalent to @Timeout(value = 5, unit = TimeUnit.SECONDS).
@Timeout on the class applies to every test method within it:
@Timeout(10)
class ApiTests {
@Test void getUsers() { ... } // max 10s
@Test void createUser() { ... } // max 10s
@Test
@Timeout(30) // overrides class-level for this method
void slowMigrationTest() { ... }
}@Timeout runs the test in a separate thread and fails preemptively — it does not wait for the test to finish. This is the same behaviour as assertTimeoutPreemptively from Chapter 2, but applied declaratively.
Set a global default in junit-platform.properties:
junit.jupiter.execution.timeout.default=10s
junit.jupiter.execution.timeout.testmethod.default=5sassertTimeout vs @Timeout
assertTimeout | assertTimeoutPreemptively | @Timeout | |
|---|---|---|---|
| Where | Inside test method | Inside test method | Annotation |
| Preemptive stop | No — waits for code | Yes — kills thread | Yes — kills thread |
| Returns value | Yes — returns executable result | Yes | No |
| Scope | One code block | One code block | Whole test method |
Use assertTimeout when you need the return value of the timed code block. Use assertTimeoutPreemptively for a hard deadline on a code block. Use @Timeout as a safety net on the whole method — especially useful as a class-level default for slow test suites.
Enabling parallel execution
By default JUnit 5 runs tests sequentially. Parallel execution is opt-in via src/test/resources/junit-platform.properties:
# Enable parallel execution
junit.jupiter.execution.parallel.enabled=true
# Default mode for test methods within a class
junit.jupiter.execution.parallel.mode.default=concurrent
# Default mode for top-level test classes
junit.jupiter.execution.parallel.mode.classes.default=concurrent
# Thread pool size strategy
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4With these settings, JUnit runs up to 4 test methods concurrently across all classes. A 20-test suite that takes 60 seconds sequentially can run in ~15 seconds with four threads.
Per-class and per-method execution mode
Override the global mode on individual classes or methods:
import org.junit.jupiter.api.parallel.*;
// Force concurrent execution of methods in this class
@Execution(ExecutionMode.CONCURRENT)
class ParallelApiTest {
@Test void getUsers() { ... }
@Test void createUser() { ... }
@Test void deleteUser() { ... }
// All three can run simultaneously
}
// Force sequential — useful for stateful workflow tests
@Execution(ExecutionMode.SAME_THREAD)
class OrderWorkflowTest {
@Test @Order(1) void createOrder() { ... }
@Test @Order(2) void payOrder() { ... }
@Test @Order(3) void shipOrder() { ... }
}Thread safety — the critical concern
When tests run in parallel, instance fields are shared if you're using @TestInstance(PER_CLASS). Even without PER_CLASS, static fields in test classes or extensions are shared across all concurrent tests. Use ThreadLocal for resources that must be per-thread:
// Thread-safe WebDriver management in parallel Selenium tests
public class ThreadLocalWebDriverExtension
implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
@Override
public void beforeEach(ExtensionContext context) {
DRIVER.set(new ChromeDriver());
}
@Override
public void afterEach(ExtensionContext context) {
WebDriver driver = DRIVER.get();
if (driver != null) {
driver.quit();
DRIVER.remove();
}
}
@Override
public boolean supportsParameter(ParameterResolutionContext param, ExtensionContext ext) {
return param.getParameter().getType() == WebDriver.class;
}
@Override
public Object resolveParameter(ParameterResolutionContext param, ExtensionContext ext) {
return DRIVER.get();
}
}Each thread gets its own WebDriver. The remove() call in afterEach prevents memory leaks — without it, the ThreadLocal entry persists for the thread's lifetime in the pool.
@ResourceLock — preventing concurrent access to shared resources
When two tests must not run simultaneously — because they both write to the same database table, or both modify a shared file — annotate them with @ResourceLock:
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
@Test
@ResourceLock("orders-table")
void testCreateOrder() {
// Only one test with @ResourceLock("orders-table") runs at a time
db.insert("orders", order);
assertEquals(1, db.count("orders"));
}
@Test
@ResourceLock("orders-table")
void testDeleteOrder() {
// Waits for testCreateOrder to finish before starting
db.delete("orders", orderId);
assertEquals(0, db.count("orders"));
}
@Test
void testGetUsers() {
// No @ResourceLock — runs concurrently with other unlocked tests
assertFalse(db.findAll("users").isEmpty());
}Two tests sharing the same lock name are serialised. Tests without the lock (or with a different lock name) still run concurrently. The ResourceAccessMode.READ_WRITE (default) gives exclusive access; ResourceAccessMode.READ allows concurrent readers:
@ResourceLock(value = "config", mode = ResourceAccessMode.READ)
void readConfig() { ... } // concurrent with other READ locks
@ResourceLock(value = "config", mode = ResourceAccessMode.READ_WRITE)
void updateConfig() { ... } // exclusiveSequential vs parallel execution
Sequential vs parallel test execution
Sequential (default)
Tests run one at a time
Simple, predictable, easy to debug — no thread interference.
Safe for shared state
Instance fields, static fields, and DB state are never accessed concurrently.
Slow for large suites
20 tests × 5 seconds each = 100 seconds. Parallelism can cut this to 25s with 4 threads.
No @ResourceLock needed
Serialised by default — locking primitives have no effect.
Parallel (opt-in)
Tests run concurrently
Multiple threads execute test methods simultaneously — up to parallelism count.
Dramatic speed improvement
API test suites with network I/O benefit most — threads wait on I/O, not CPU.
Requires thread-safe code
Shared static fields, ThreadLocal resources, and @ResourceLock for DB contention.
Configure in junit-platform.properties
One file enables it project-wide; @Execution overrides per class or method.
⚠️ Common mistakes
- Enabling parallel execution without auditing for shared static state. The most common parallel failure is a static field written by one test and read by another concurrently. Before enabling parallelism, grep for
staticfields in your test classes and verify they are either immutable or replaced withThreadLocal. @ResourceLockwith different lock name strings that look the same.@ResourceLock("Database")and@ResourceLock("database")are different locks. Tests using each will run concurrently — they do not share a lock. Centralise lock names as constants in a shared class to avoid silent mismatches.@Timeouton@BeforeAllsetup that legitimately takes long. If your database migration takes 30 seconds on first run, a 10-second class-level timeout fails the@BeforeAll. Either exempt setup methods from the timeout or set the class-level timeout generously and rely on method-level timeouts for the individual tests.
🎯 Practice task
Enable parallel execution and guard shared resources. 25–35 minutes.
- Create
src/test/resources/junit-platform.propertieswithjunit.jupiter.execution.parallel.enabled=true,mode.default=concurrent, andparallelism=4. - Write a test class with four
@Testmethods, each withThread.sleep(1000)inside. Run with parallelism disabled (comment out the properties), record the time. Re-enable parallelism, run again. Confirm it runs in roughly a quarter of the time. - Introduce a race condition. Add a static
int counter = 0field. Each test doescounter++; assertEquals(expected, counter). Run in parallel and observe the test failures caused by the race. Then fix it: useAtomicIntegerand adjust assertions. @ResourceLock. Create two tests that both read and write to a sharedList. Add@ResourceLock("shared-list")to both. Confirm they stop failing when parallelism is enabled.@Timeout. Add@Timeout(2)to the class. Write one test withThread.sleep(3000)— confirm it fails with a timeout. Write another withThread.sleep(1000)— confirm it passes.- Stretch —
ThreadLocalWebDriver. ImplementThreadLocalWebDriverExtensionfrom the lesson. Write three parallel Selenium tests, each receiving aWebDriverparameter. AddSystem.out.println(Thread.currentThread().getName())inside each test — confirm three different thread names appear, proving concurrent execution.
This completes Chapter 4. Chapter 5 brings it all together with Selenium WebDriver, Maven Surefire configuration, and test report generation.