Advanced Topics
Advanced features and patterns for power users
This section covers advanced Verifyica features for sophisticated testing scenarios.
Topics
When to Use Advanced Features
These features are powerful but add complexity. Use them when:
- Standard lifecycle isn’t sufficient
- Tests have complex dependencies
- Need fine-grained execution control
- Managing shared resources across tests
Best Practices
- Start with simple patterns before adding complexity
- Document why advanced features are needed
- Test advanced patterns thoroughly
- Consider maintainability impact
1 - Advanced Parallelism
Advanced parallel execution patterns and strategies
This page covers advanced parallelism patterns beyond the basics. See Configuration → Parallelism for basic configuration.
Resource Pooling with Parallel Arguments
When running arguments in parallel with limited resources, use a pool:
public class PooledResourceTest {
private static final Semaphore resourcePool = new Semaphore(3); // Max 3 concurrent
public static class TestContext {
private final Resource resource;
public TestContext(Resource resource) {
this.resource = resource;
}
public Resource getResource() {
return resource;
}
}
@Verifyica.ArgumentSupplier(parallelism = 10)
public static Object arguments() {
return IntStream.range(0, 10)
.mapToObj(i -> "arg-" + i)
.collect(Collectors.toList());
}
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) throws InterruptedException {
// Acquire from pool (blocks if pool is full)
resourcePool.acquire();
Resource resource = ResourcePool.getInstance().acquire();
TestContext context = new TestContext(resource);
argumentContext.getMap().put("testContext", context);
}
@Verifyica.Test
public void test(ArgumentContext argumentContext) {
TestContext context = (TestContext) argumentContext.getMap().get("testContext");
context.getResource().use();
}
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) {
TestContext context = (TestContext) argumentContext.getMap().get("testContext");
if (context != null && context.getResource() != null) {
ResourcePool.getInstance().release(context.getResource());
resourcePool.release(); // Return to pool
}
}
}
Dynamic Port Allocation
Avoid port conflicts when running parallel tests:
public class PortAllocationTest {
private static final AtomicInteger portCounter = new AtomicInteger(8000);
public static class TestContext {
private final int port;
private final Server server;
public TestContext(int port, Server server) {
this.port = port;
this.server = server;
}
public Server getServer() {
return server;
}
}
@Verifyica.ArgumentSupplier(parallelism = 5)
public static Object arguments() {
return List.of("service-1", "service-2", "service-3", "service-4", "service-5");
}
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
// Allocate unique port for this argument
int port = portCounter.getAndIncrement();
Server server = new Server(port);
server.start();
TestContext context = new TestContext(port, server);
argumentContext.getMap().put("testContext", context);
}
@Verifyica.Test
public void testService(ArgumentContext argumentContext) {
TestContext context = (TestContext) argumentContext.getMap().get("testContext");
HttpClient client = new HttpClient("localhost", context.getServer().getPort());
Response response = client.get("/health");
assert response.getStatus() == 200;
}
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) {
TestContext context = (TestContext) argumentContext.getMap().get("testContext");
if (context != null && context.getServer() != null) {
context.getServer().stop();
}
}
}
Partitioned Data Processing
Divide large datasets across parallel arguments:
public class DataPartitionTest {
@Verifyica.ArgumentSupplier(parallelism = 4)
public static Object arguments() {
List<Integer> allData = IntStream.range(0, 1000)
.boxed()
.collect(Collectors.toList());
// Partition into 4 chunks
int partitionSize = allData.size() / 4;
List<Argument<List<Integer>>> partitions = new ArrayList<>();
for (int i = 0; i < 4; i++) {
int start = i * partitionSize;
int end = (i == 3) ? allData.size() : (i + 1) * partitionSize;
List<Integer> partition = allData.subList(start, end);
partitions.add(Argument.of("partition-" + i, partition));
}
return partitions;
}
@Verifyica.Test
public void processPartition(List<Integer> partition) {
// Each argument processes its partition in parallel
partition.forEach(this::processItem);
}
private void processItem(int item) {
// Process individual item
}
}
Coordinating Parallel Tests
Use CountDownLatch to synchronize parallel arguments:
public class CoordinatedTest {
private static final CountDownLatch readyLatch = new CountDownLatch(3);
private static final CountDownLatch startLatch = new CountDownLatch(1);
@Verifyica.ArgumentSupplier(parallelism = 3)
public static Object arguments() {
return List.of("client-1", "client-2", "client-3");
}
@Verifyica.Test
public void coordinatedTest(String client) throws InterruptedException {
// Signal ready
System.out.println(client + " is ready");
readyLatch.countDown();
// Wait for all to be ready
readyLatch.await();
// All start together
System.out.println(client + " starting test");
performTest(client);
}
}
Mixing Sequential and Parallel Execution
Run some arguments sequentially, others in parallel:
public class MixedExecutionTest {
@Verifyica.ArgumentSupplier(parallelism = 1) // Sequential
public static Object criticalArguments() {
return List.of(
Argument.of("production-db", new DbConfig("prod"))
);
}
// In another test class with parallelism
@Verifyica.ArgumentSupplier(parallelism = 4) // Parallel
public static Object testArguments() {
return List.of(
Argument.of("test-db-1", new DbConfig("test1")),
Argument.of("test-db-2", new DbConfig("test2")),
Argument.of("test-db-3", new DbConfig("test3")),
Argument.of("test-db-4", new DbConfig("test4"))
);
}
}
Monitoring Parallel Execution
Track parallel test execution with metrics:
public class MonitoredParallelTest {
private static final AtomicInteger activeTests = new AtomicInteger(0);
private static final AtomicInteger completedTests = new AtomicInteger(0);
@Verifyica.ArgumentSupplier(parallelism = 8)
public static Object arguments() {
return IntStream.range(0, 20)
.mapToObj(i -> "arg-" + i)
.collect(Collectors.toList());
}
@Verifyica.BeforeAll
public void beforeAll(String argument) {
int active = activeTests.incrementAndGet();
System.out.println("Active tests: " + active + " (" + argument + ")");
}
@Verifyica.Test
public void test(String argument) {
// Test logic
}
@Verifyica.AfterAll
public void afterAll(String argument) {
activeTests.decrementAndGet();
int completed = completedTests.incrementAndGet();
System.out.println("Completed tests: " + completed + " (" + argument + ")");
}
}
Best Practices
Choose Appropriate Parallelism
// Good: Match parallelism to resources
@Verifyica.ArgumentSupplier(parallelism = 4) // 4 CPU cores
public static Object arguments() {
return getCpuBoundTests();
}
// Good: Higher parallelism for I/O bound
@Verifyica.ArgumentSupplier(parallelism = 20) // I/O bound
public static Object arguments() {
return getNetworkTests();
}
Avoid Excessive Parallelism
// Bad: Too much parallelism
@Verifyica.ArgumentSupplier(parallelism = 100)
public static Object arguments() {
return List.of("test"); // Only 1 argument!
}
Clean Up Resources
Always clean up in @AfterAll, even with failures:
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) {
TestContext context = (TestContext) argumentContext.getMap().get("testContext");
if (context != null) {
try {
context.getResource().close();
} catch (Exception e) {
// Log but don't fail cleanup
logger.warn("Cleanup failed", e);
}
}
}
See Also
2 - Test Dependencies
Define test dependencies with @DependsOn
The @DependsOn annotation allows you to specify that a test method should only run if another test method passes.
Overview
Use @DependsOn when a test requires another test to complete successfully first. If the dependency fails or is skipped, the dependent test is also skipped.
Basic Usage
import org.verifyica.api.DependsOn;
import org.verifyica.api.Verifyica;
public class DependencyTest {
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of("test-data");
}
@Verifyica.Test
public void prerequisite(String argument) {
// This must pass for dependent tests to run
setupEnvironment();
}
@Verifyica.Test
@DependsOn("prerequisite")
public void dependentTest(String argument) {
// Only runs if prerequisite passes
performMainTest();
}
}
Multiple Dependencies
A test can depend on multiple other tests:
@Verifyica.Test
public void setup1() {
// First setup
}
@Verifyica.Test
public void setup2() {
// Second setup
}
@Verifyica.Test
@DependsOn({"setup1", "setup2"})
public void mainTest() {
// Only runs if BOTH setup1 AND setup2 pass
}
Dependency Chains
You can create chains of dependencies:
@Verifyica.Test
public void createDatabase() {
database.create();
}
@Verifyica.Test
@DependsOn("createDatabase")
public void createTables() {
database.createTables();
}
@Verifyica.Test
@DependsOn("createTables")
public void insertData() {
database.insertData();
}
@Verifyica.Test
@DependsOn("insertData")
public void verifyData() {
assert database.countRows() > 0;
}
Example: Environment Setup
public class IntegrationTest {
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of(
Argument.of("dev", new Environment("dev")),
Argument.of("staging", new Environment("staging"))
);
}
@Verifyica.Test
public void checkConnection(ArgumentContext argumentContext) {
Environment env = argumentContext.getArgument().getPayloadAs(Environment.class);
assert env.isReachable();
}
@Verifyica.Test
@DependsOn("checkConnection")
public void authenticate(ArgumentContext argumentContext) {
// Only runs if connection check passes
Environment env = argumentContext.getArgument().getPayloadAs(Environment.class);
assert env.authenticate();
}
@Verifyica.Test
@DependsOn("authenticate")
public void runTests(ArgumentContext argumentContext) {
// Only runs if authentication succeeds
performIntegrationTests();
}
}
Behavior When Dependencies Fail
- If a dependency fails, dependent tests are skipped
- If a dependency is skipped, dependent tests are skipped
- If a dependency passes, dependent tests run normally
Example:
@Verifyica.Test
public void mayFail() {
if (random.nextBoolean()) {
throw new RuntimeException("Failed");
}
}
@Verifyica.Test
@DependsOn("mayFail")
public void dependent() {
// Skipped if mayFail() throws exception
}
Combining with @Order
Use both annotations together for explicit ordering:
@Verifyica.Test
@Order(1)
public void first() {
// Runs first
}
@Verifyica.Test
@Order(2)
@DependsOn("first")
public void second() {
// Runs second (if first passes)
}
Dependencies Per Argument
Dependencies are evaluated per argument independently:
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of("arg1", "arg2");
}
@Verifyica.Test
public void setup(String arg) {
// For arg1: might pass
// For arg2: might fail
}
@Verifyica.Test
@DependsOn("setup")
public void main(String arg) {
// For arg1: runs if setup(arg1) passed
// For arg2: skipped if setup(arg2) failed
}
Best Practices
Use for True Prerequisites
// Good: Genuine prerequisite
@Verifyica.Test
public void databaseAvailable() {
assert database.ping();
}
@Verifyica.Test
@DependsOn("databaseAvailable")
public void testQueries() {
// Makes sense to skip if DB is unavailable
}
// Less ideal: Creating unnecessary coupling
@Verifyica.Test
public void testA() { }
@Verifyica.Test
@DependsOn("testA")
public void testB() { } // Why does B depend on A?
Avoid Circular Dependencies
// Bad: Circular dependency (not allowed)
@Verifyica.Test
@DependsOn("test2")
public void test1() { }
@Verifyica.Test
@DependsOn("test1")
public void test2() { }
// This will cause an error
Keep Dependency Chains Short
// Good: Short chain
@DependsOn("setup")
// Less ideal: Long chain
@DependsOn("a")
public void b() { }
@DependsOn("b")
public void c() { }
@DependsOn("c")
public void d() { }
// Hard to understand and maintain
See Also
- Ordering - @Order for controlling execution sequence
- Lifecycle - Test lifecycle phases
3 - Test Ordering
Control test method execution order with @Order
The @Order annotation allows you to specify the execution order of test methods within an argument.
Overview
By default, test methods execute in an undefined order. Use @Order when tests must run in a specific sequence.
Basic Usage
import org.verifyica.api.Order;
import org.verifyica.api.Verifyica;
public class OrderedTest {
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of("test-data");
}
@Verifyica.Test
@Order(1)
public void firstTest(String argument) {
System.out.println("Runs first");
}
@Verifyica.Test
@Order(2)
public void secondTest(String argument) {
System.out.println("Runs second");
}
@Verifyica.Test
@Order(3)
public void thirdTest(String argument) {
System.out.println("Runs third");
}
}
Order Values
- Lower values execute first (e.g.,
@Order(1) before @Order(2)) - Methods without
@Order have default order value Integer.MAX_VALUE - Methods with the same order value execute in undefined order
Example: Setup and Teardown Sequence
public class DatabaseMigrationTest {
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of(Argument.of("production-db", new DbConfig()));
}
@Verifyica.Test
@Order(1)
public void createTables(ArgumentContext argumentContext) {
// Must run first
database.createTables();
}
@Verifyica.Test
@Order(2)
public void insertData(ArgumentContext argumentContext) {
// Runs after tables are created
database.insertTestData();
}
@Verifyica.Test
@Order(3)
public void verifyData(ArgumentContext argumentContext) {
// Runs after data is inserted
assert database.countRows() > 0;
}
}
Combining with @DependsOn
You can use both @Order and @DependsOn together:
@Verifyica.Test
@Order(1)
public void setupTest() {
// Runs first
}
@Verifyica.Test
@Order(2)
@DependsOn("setupTest")
public void mainTest() {
// Runs second, and only if setupTest passes
}
Best Practices
Use Order Sparingly
// Good: Use only when necessary
@Verifyica.Test
@Order(1)
public void initialize() { }
// Bad: Don't order everything
@Verifyica.Test
@Order(1)
public void test1() { }
@Verifyica.Test
@Order(2)
public void test2() { }
@Verifyica.Test
@Order(3)
public void test3() { }
Prefer Test Independence
Independent tests are better than ordered tests:
// Good: Independent tests
@Verifyica.Test
public void testCreate() {
User user = createUser();
assert user != null;
}
@Verifyica.Test
public void testUpdate() {
User user = createUser(); // Creates its own user
user.setName("Updated");
assert user.getName().equals("Updated");
}
// Less ideal: Dependent tests
@Verifyica.Test
@Order(1)
public void testCreate() {
sharedUser = createUser();
}
@Verifyica.Test
@Order(2)
public void testUpdate() {
sharedUser.setName("Updated"); // Depends on testCreate
}
Leave Gaps in Order Numbers
// Good: Gaps allow inserting tests later
@Order(10)
public void test1() { }
@Order(20)
public void test2() { }
@Order(30)
public void test3() { }
// Now you can add @Order(15) without renumbering
Execution Order with Multiple Arguments
Order applies to each argument independently:
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of("arg1", "arg2");
}
@Verifyica.Test
@Order(1)
public void first(String arg) { }
@Verifyica.Test
@Order(2)
public void second(String arg) { }
Execution sequence:
arg1: first(arg1) → second(arg1)
arg2: first(arg2) → second(arg2)
See Also
4 - Test Tagging
Organize and filter tests with @Tag
The @Tag annotation allows you to label tests for filtering and organization.
Overview
Tags provide a flexible way to categorize tests so you can run specific subsets during different testing scenarios.
Basic Usage
import org.verifyica.api.Tag;
import org.verifyica.api.Verifyica;
@Tag("integration")
public class IntegrationTest {
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of("test-data");
}
@Verifyica.Test
public void testDatabaseConnection(String argument) {
// Tagged as "integration" from class level
}
}
Apply multiple tags to organize tests by different dimensions:
@Tag("integration")
@Tag("database")
@Tag("slow")
public class DatabaseIntegrationTest {
@Verifyica.Test
public void testQuery(String argument) {
// Has all three tags: integration, database, slow
}
}
Override or extend class-level tags on individual methods:
@Tag("integration")
public class MixedTest {
@Verifyica.ArgumentSupplier
public static Object arguments() {
return List.of("test-data");
}
@Verifyica.Test
@Tag("fast")
public void quickTest(String argument) {
// Tags: integration, fast
}
@Verifyica.Test
@Tag("slow")
public void slowTest(String argument) {
// Tags: integration, slow
}
}
Common Tag Categories
By Test Type
@Tag("unit") // Unit tests
@Tag("integration") // Integration tests
@Tag("e2e") // End-to-end tests
@Tag("smoke") // Smoke tests
By Speed
@Tag("fast") // Quick tests
@Tag("slow") // Slow tests
@Tag("overnight") // Very long tests
By Component
@Tag("database")
@Tag("api")
@Tag("ui")
@Tag("security")
By Environment
@Tag("dev")
@Tag("staging")
@Tag("production")
Example: Organizing Test Suites
@Tag("integration")
@Tag("database")
public class DatabaseTest {
@Verifyica.Test
@Tag("fast")
public void testConnection(String argument) {
// Fast database connection test
}
@Verifyica.Test
@Tag("slow")
public void testComplexQuery(String argument) {
// Slow complex query test
}
}
@Tag("integration")
@Tag("api")
public class ApiTest {
@Verifyica.Test
@Tag("fast")
public void testEndpoint(String argument) {
// Fast API endpoint test
}
}
Configure filters in verifyica.yaml:
# Run only fast integration tests
filters:
include:
tags:
- integration
- fast
# Run all tests except slow ones
filters:
exclude:
tags:
- slow
# Run database tests but not overnight ones
filters:
include:
tags:
- database
exclude:
tags:
- overnight
See Configuration → Filters for complete filtering options.
Tag Naming Conventions
Use Lowercase
// Good
@Tag("integration")
@Tag("database")
// Less ideal
@Tag("Integration")
@Tag("DATABASE")
Use Descriptive Names
// Good
@Tag("requires-network")
@Tag("writes-to-filesystem")
// Less ideal
@Tag("rn")
@Tag("fs")
Avoid Spaces
// Good
@Tag("end-to-end")
@Tag("smoke-test")
// Bad
@Tag("end to end")
@Tag("smoke test")
@Tag("pr-check")
public class PullRequestTest {
// Runs on every pull request
}
@Tag("nightly")
public class ComprehensiveTest {
// Runs once per night
}
@Tag("release")
public class ReleaseValidationTest {
// Runs before releases
}
Configure CI to run different tag sets:
# PR builds: Run fast smoke tests
mvn test -Dverifyica.filter.include.tags=pr-check,fast
# Nightly builds: Run comprehensive suite
mvn test -Dverifyica.filter.include.tags=nightly
# Release builds: Run release validation
mvn test -Dverifyica.filter.include.tags=release
Best Practices
Tag Strategically
// Good: Meaningful categorization
@Tag("integration")
@Tag("database")
@Tag("slow")
// Less useful: Over-tagging
@Tag("test")
@Tag("java")
@Tag("class")
Consistent Naming
Establish team conventions:
// Good: Consistent naming
@Tag("integration")
@Tag("integration-api")
@Tag("integration-database")
// Inconsistent
@Tag("integration")
@Tag("apiIntegration")
@Tag("db_integration")
Document Tag Meanings
Create a tag catalog in your project README:
## Test Tags
- `fast` - Tests that run in < 1 second
- `slow` - Tests that take > 10 seconds
- `integration` - Tests requiring external systems
- `database` - Tests requiring database
- `pr-check` - Must pass before merging PRs
See Also
5 - Cleanup Executor
Advanced cleanup patterns with CleanupExecutor
The CleanupExecutor utility helps manage cleanup tasks that must execute in reverse order, even when exceptions occur.
Overview
CleanupExecutor is a specialized utility that:
- Executes cleanup tasks in reverse order (LIFO - Last In, First Out)
- Continues executing all cleanup tasks even if some fail
- Collects all exceptions that occur during cleanup
Basic Usage
import org.verifyica.api.CleanupExecutor;
public class CleanupTest {
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor = new CleanupExecutor();
try {
// Setup resources and register cleanup
Connection conn = database.connect();
cleanupExecutor.add(() -> conn.close());
Server server = new Server();
server.start();
cleanupExecutor.add(() -> server.stop());
// Store cleanup executor in context
argumentContext.getMap().put("cleanup", cleanupExecutor);
} catch (Exception e) {
// If setup fails, execute cleanup
cleanupExecutor.execute();
throw e;
}
}
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) throws Throwable {
CleanupExecutor cleanupExecutor =
(CleanupExecutor) argumentContext.getMap().get("cleanup");
if (cleanupExecutor != null) {
cleanupExecutor.execute();
}
}
}
Example: Nested Resource Setup
public class NestedResourceTest {
public static class TestContext {
private final CleanupExecutor cleanupExecutor;
public TestContext(CleanupExecutor cleanupExecutor) {
this.cleanupExecutor = cleanupExecutor;
}
public CleanupExecutor getCleanupExecutor() {
return cleanupExecutor;
}
}
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor = new CleanupExecutor();
try {
// 1. Create database
Database db = Database.create("test-db");
cleanupExecutor.add(() -> db.drop());
// 2. Connect to database
Connection conn = db.connect();
cleanupExecutor.add(() -> conn.close());
// 3. Create schema
conn.execute("CREATE TABLE users (id INT, name VARCHAR(100))");
cleanupExecutor.add(() -> conn.execute("DROP TABLE users"));
// 4. Insert test data
conn.execute("INSERT INTO users VALUES (1, 'Alice')");
cleanupExecutor.add(() -> conn.execute("DELETE FROM users"));
TestContext context = new TestContext(cleanupExecutor);
argumentContext.getMap().put("testContext", context);
} catch (Exception e) {
cleanupExecutor.execute();
throw new RuntimeException("Setup failed", e);
}
}
@Verifyica.Test
public void test(ArgumentContext argumentContext) {
// Test logic
}
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) throws Throwable {
TestContext context = (TestContext) argumentContext.getMap().get("testContext");
if (context != null && context.getCleanupExecutor() != null) {
// Executes in reverse order:
// 4. DELETE FROM users
// 3. DROP TABLE users
// 2. conn.close()
// 1. db.drop()
context.getCleanupExecutor().execute();
}
}
}
Error Handling
CleanupExecutor collects all exceptions:
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor =
(CleanupExecutor) argumentContext.getMap().get("cleanup");
if (cleanupExecutor != null) {
try {
cleanupExecutor.execute();
} catch (Throwable t) {
// t contains all cleanup exceptions
logger.error("Cleanup failed", t);
throw new RuntimeException("Cleanup failed", t);
}
}
}
TestContainers Integration
Ideal for managing container lifecycles:
public class ContainerTest {
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor = new CleanupExecutor();
try {
// Start database container
GenericContainer<?> dbContainer = new GenericContainer<>("postgres:15")
.withExposedPorts(5432);
dbContainer.start();
cleanupExecutor.add(() -> dbContainer.stop());
// Start application container
GenericContainer<?> appContainer = new GenericContainer<>("myapp:latest")
.withExposedPorts(8080)
.withEnv("DB_HOST", dbContainer.getHost());
appContainer.start();
cleanupExecutor.add(() -> appContainer.stop());
argumentContext.getMap().put("cleanup", cleanupExecutor);
} catch (Exception e) {
cleanupExecutor.execute();
throw e;
}
}
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) throws Throwable {
CleanupExecutor cleanupExecutor =
(CleanupExecutor) argumentContext.getMap().get("cleanup");
if (cleanupExecutor != null) {
// Stops app container first, then db container
cleanupExecutor.execute();
}
}
}
File System Cleanup
public class FileSystemTest {
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor = new CleanupExecutor();
try {
// Create temp directory
Path tempDir = Files.createTempDirectory("test");
cleanupExecutor.add(() -> deleteDirectory(tempDir));
// Create test files
Path file1 = tempDir.resolve("file1.txt");
Files.write(file1, "content".getBytes());
cleanupExecutor.add(() -> Files.deleteIfExists(file1));
Path file2 = tempDir.resolve("file2.txt");
Files.write(file2, "content".getBytes());
cleanupExecutor.add(() -> Files.deleteIfExists(file2));
argumentContext.getMap().put("cleanup", cleanupExecutor);
} catch (Exception e) {
cleanupExecutor.execute();
throw new RuntimeException("Setup failed", e);
}
}
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) throws Throwable {
CleanupExecutor cleanupExecutor =
(CleanupExecutor) argumentContext.getMap().get("cleanup");
if (cleanupExecutor != null) {
// Deletes files first, then directory
cleanupExecutor.execute();
}
}
private void deleteDirectory(Path dir) throws IOException {
if (Files.exists(dir)) {
Files.walk(dir)
.sorted((a, b) -> b.compareTo(a)) // Reverse order
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
}
Best Practices
// Good: Register cleanup right after resource creation
Resource resource = create();
cleanupExecutor.add(() -> resource.close());
// Less ideal: Register cleanup later
Resource resource = create();
// ... lots of code ...
cleanupExecutor.add(() -> resource.close()); // Easy to forget
Handle Cleanup Exceptions
// Good: Handle and log cleanup exceptions
@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor =
(CleanupExecutor) argumentContext.getMap().get("cleanup");
if (cleanupExecutor != null) {
try {
cleanupExecutor.execute();
} catch (Throwable t) {
logger.error("Cleanup failed", t);
// Don't rethrow - cleanup failures shouldn't fail passing tests
}
}
}
Cleanup Even on Setup Failure
// Good: Execute cleanup if setup fails
@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
CleanupExecutor cleanupExecutor = new CleanupExecutor();
try {
// Setup resources
} catch (Exception e) {
cleanupExecutor.execute(); // Important!
throw e;
}
argumentContext.getMap().put("cleanup", cleanupExecutor);
}
See Also
6 - Keyed Concurrency Utilities
Advanced concurrency control with keyed utilities
Verifyica provides keyed concurrency utilities for fine-grained synchronization in parallel tests.
Overview
Keyed concurrency utilities allow you to synchronize on specific keys rather than globally:
- KeyedMutexManager - Mutual exclusion per key
- KeyedLatchManager - Count-down latches per key
- KeyedSemaphoreManager - Semaphores per key
KeyedMutexManager
Provides mutual exclusion on a per-key basis.
Basic Usage
import org.verifyica.api.concurrent.KeyedMutexManager;
public class MutexTest {
@Verifyica.ArgumentSupplier(parallelism = 10)
public static Object arguments() {
return List.of("user-1", "user-2", "user-3");
}
@Verifyica.Test
public void testConcurrentAccess(String userId) {
// Only one test can access this user at a time
KeyedMutexManager.lock(userId);
try {
updateUser(userId);
} finally {
KeyedMutexManager.unlock(userId);
}
}
}
Example: Database Row Locking
public class RowLockTest {
@Verifyica.Test
public void testRowUpdate(String rowId) {
KeyedMutexManager.lock(rowId);
try {
// Exclusive access to this row
Row row = database.getRow(rowId);
row.setValue(row.getValue() + 1);
database.updateRow(row);
} finally {
KeyedMutexManager.unlock(rowId);
}
}
}
KeyedLatchManager
Count-down latches per key for coordination.
Basic Usage
import org.verifyica.api.concurrent.KeyedLatchManager;
public class LatchTest {
@Verifyica.BeforeAll
public void beforeAll(String group) {
// Initialize latch for this group (3 tests must complete)
KeyedLatchManager.createLatch(group, 3);
}
@Verifyica.Test
public void test1(String group) throws InterruptedException {
performTest1();
KeyedLatchManager.countDown(group);
// Wait for all 3 tests to complete
KeyedLatchManager.await(group);
// Now all tests have completed
verifyResults(group);
}
}
Example: Barrier Synchronization
public class BarrierTest {
@Verifyica.ArgumentSupplier(parallelism = 5)
public static Object arguments() {
return List.of("batch-1", "batch-2");
}
@Verifyica.BeforeAll
public void beforeAll(String batch) {
// Each batch has 5 tests that must sync
KeyedLatchManager.createLatch(batch, 5);
}
@Verifyica.Test
public void coordinatedTest(String batch) throws InterruptedException {
// Phase 1: Prepare
prepare();
// Signal ready and wait for others
KeyedLatchManager.countDown(batch);
KeyedLatchManager.await(batch);
// Phase 2: All execute together
executeTest();
}
}
KeyedSemaphoreManager
Semaphores per key for resource limiting.
Basic Usage
import org.verifyica.api.concurrent.KeyedSemaphoreManager;
public class SemaphoreTest {
@Verifyica.BeforeAll
public void beforeAll(String resource) {
// Limit to 3 concurrent accesses per resource
KeyedSemaphoreManager.createSemaphore(resource, 3);
}
@Verifyica.Test
public void testLimitedAccess(String resource) throws InterruptedException {
KeyedSemaphoreManager.acquire(resource);
try {
// Max 3 tests can access this resource concurrently
accessResource(resource);
} finally {
KeyedSemaphoreManager.release(resource);
}
}
}
Example: API Rate Limiting
public class RateLimitTest {
@Verifyica.BeforeAll
public void beforeAll(String apiEndpoint) {
// Each endpoint allows 10 concurrent requests
KeyedSemaphoreManager.createSemaphore(apiEndpoint, 10);
}
@Verifyica.Test
public void testApiCall(String apiEndpoint) throws InterruptedException {
KeyedSemaphoreManager.acquire(apiEndpoint);
try {
ApiClient client = new ApiClient();
Response response = client.call(apiEndpoint);
assert response.getStatus() == 200;
} finally {
KeyedSemaphoreManager.release(apiEndpoint);
}
}
}
Combined Example: Multi-Level Coordination
public class ComplexCoordinationTest {
@Verifyica.ArgumentSupplier(parallelism = 20)
public static Object arguments() {
List<Argument<TestData>> args = new ArrayList<>();
for (int i = 0; i < 100; i++) {
String userId = "user-" + (i % 10); // 10 unique users
String pool = "pool-" + (i % 5); // 5 pools
String phase = "phase-1"; // All in same phase
args.add(Argument.of("test-" + i, new TestData(userId, pool, phase)));
}
return args;
}
@Verifyica.BeforeAll
public void beforeAll(TestData data) {
// Limit concurrent access per pool
KeyedSemaphoreManager.createSemaphore(data.getPool(), 5);
// Initialize phase gate for coordination
KeyedLatchManager.createLatch(data.getPhase(), 100);
}
@Verifyica.Test
public void complexTest(TestData data) throws InterruptedException {
// 1. Acquire pool permit (max 5 per pool)
KeyedSemaphoreManager.acquire(data.getPool());
try {
// 2. Lock specific user (exclusive access)
KeyedMutexManager.lock(data.getUserId());
try {
performUserOperation(data.getUserId());
} finally {
KeyedMutexManager.unlock(data.getUserId());
}
// 3. Signal phase completion
KeyedLatchManager.countDown(data.getPhase());
// 4. Wait for all to complete phase
KeyedLatchManager.await(data.getPhase());
// 5. All proceed together
performCoordinatedOperation(data);
} finally {
KeyedSemaphoreManager.release(data.getPool());
}
}
}
Best Practices
Always Use Try-Finally
// Good: Always unlock/release in finally
KeyedMutexManager.lock(key);
try {
// Critical section
} finally {
KeyedMutexManager.unlock(key);
}
// Bad: Might not unlock on exception
KeyedMutexManager.lock(key);
// Critical section
KeyedMutexManager.unlock(key); // Might not execute!
Clean Up Resources
@Verifyica.Conclude
public void conclude(ClassContext classContext) {
// Note: KeyedMutexManager, KeyedLatchManager, and KeyedSemaphoreManager
// automatically clean up keys when they are no longer in use.
// Manual cleanup is typically not required.
}
Avoid Deadlocks
// Bad: Can cause deadlock
KeyedMutexManager.lock(key1);
KeyedMutexManager.lock(key2); // Another thread might lock in reverse order
// Good: Lock in consistent order
List<String> keys = List.of(key1, key2);
Collections.sort(keys);
for (String key : keys) {
KeyedMutexManager.lock(key);
}
try {
// Critical section
} finally {
for (String key : keys) {
KeyedMutexManager.unlock(key);
}
}
Set Appropriate Timeouts
// Good: Use timeouts to avoid infinite waits
if (!KeyedLatchManager.await(key, 30, TimeUnit.SECONDS)) {
throw new TimeoutException("Coordination timed out");
}
See Also