This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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
    }
}

Multiple Tags

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
    }
}

Method-Level Tags

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
    }
}

Filtering Tests by Tags

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")

Example: CI/CD Pipeline Tags

@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

Register Cleanup Immediately

// 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