Management of container and resource lifecycles for coordinated startup and shutdown operations with dependency resolution and test lifecycle integration.
Core interface for any resource that can be started and stopped, with support for dependency management. This is the fundamental abstraction used by all Testcontainers for lifecycle control.
/**
* Represents a resource that can be started and stopped with automatic dependency management.
* This is a core abstraction for coordinating the lifecycle of containers and other resources.
*/
public interface Startable extends AutoCloseable {
/**
* Start the resource.
* Implementations should ensure all dependencies are started first.
* This method may block until the resource is fully started.
*/
void start();
/**
* Stop the resource.
* Implementations should clean up all resources allocated during start().
* This method is called automatically by close().
*/
void stop();
/**
* Get the dependencies that must be started before this resource.
* Returns an empty set by default, but can be overridden to declare dependencies.
*
* @return set of Startable instances that must be started first (default: empty set)
*/
default Set<Startable> getDependencies() {
return Collections.emptySet();
}
/**
* Close this resource (implements AutoCloseable).
* Default implementation calls stop().
*/
@Override
default void close() {
stop();
}
}Implemented By:
GenericContainer - All container types in TestcontainersNetwork - Container networksUsage Examples:
import org.testcontainers.lifecycle.Startable;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
// All GenericContainer instances implement Startable
Startable container = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379);
// Start and stop
container.start();
try {
// Use container
} finally {
container.stop();
}
// Or use with try-with-resources (since it implements AutoCloseable)
try (Startable container = new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432)) {
container.start();
// Use container
} // Automatically stopped and closed
// Declare dependencies
Startable database = new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432);
Startable application = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withEnv("DB_HOST", "db")
.dependsOn(database);
// Start application and its dependencies
application.start(); // Database is started firstCustom Startable Implementation:
import org.testcontainers.lifecycle.Startable;
import java.util.Collections;
import java.util.Set;
public class CustomResource implements Startable {
private String resourceId;
@Override
public void start() {
// Allocate resource
this.resourceId = allocateResource();
System.out.println("Resource started: " + resourceId);
}
@Override
public void stop() {
// Clean up resource
if (resourceId != null) {
releaseResource(resourceId);
resourceId = null;
System.out.println("Resource stopped");
}
}
@Override
public Set<Startable> getDependencies() {
return Collections.emptySet();
}
private String allocateResource() {
return "resource-" + System.nanoTime();
}
private void releaseResource(String id) {
// Cleanup logic
}
}
// Usage
try (Startable resource = new CustomResource()) {
resource.start();
// Use resource
}Utility class for managing multiple Startable instances with parallel execution and automatic dependency resolution. This is the recommended way to start multiple containers efficiently.
/**
* Utility class for managing multiple Startable instances with dependency resolution.
* Uses CompletableFuture for asynchronous, parallel startup while respecting dependencies.
* This is the primary way to manage coordinated container lifecycle in production code.
*/
@UtilityClass
public class Startables {
/**
* Start multiple resources asynchronously, respecting their dependencies.
* Resources are started in parallel where possible, but dependencies are honored.
*
* Performance characteristics:
* - All resources with no dependencies start in parallel
* - As soon as a resource completes, any dependent resources can start
* - Returns a CompletableFuture that completes when all resources are started
*
* Example dependency graph:
* a -> b -> e
* /
* c
* d ----/
* Result: a, c, d start in parallel, then b starts, then e starts
*
* @param startables variable argument list of resources to start
* @return CompletableFuture<Void> that completes when all resources are started
* @throws CompletionException (wrapping underlying exception) if any resource fails to start
*/
public static CompletableFuture<Void> deepStart(Startable... startables);
/**
* Start multiple resources from a Collection asynchronously, respecting dependencies.
* Equivalent to deepStart(collection.stream()).
*
* @param startables collection of resources to start
* @return CompletableFuture<Void> that completes when all resources are started
* @throws CompletionException (wrapping underlying exception) if any resource fails to start
*/
public static CompletableFuture<Void> deepStart(Collection<? extends Startable> startables);
/**
* Start multiple resources from an Iterable asynchronously, respecting dependencies.
* Equivalent to deepStart(StreamSupport.stream(iterable.spliterator(), false)).
*
* @param startables iterable of resources to start
* @return CompletableFuture<Void> that completes when all resources are started
* @throws CompletionException (wrapping underlying exception) if any resource fails to start
*/
public static CompletableFuture<Void> deepStart(Iterable<? extends Startable> startables);
/**
* Start multiple resources from a Stream asynchronously, respecting their dependencies.
* This is the primary implementation; other methods delegate to this.
*
* Dependency resolution:
* 1. For each Startable in the stream, getDependencies() is called recursively
* 2. A cache prevents starting the same resource multiple times
* 3. Dependencies are started before the dependent using thenRunAsync()
* 4. All independent resources start in parallel using a cached thread pool
* 5. Failures fail-fast: first exception completes the returned future exceptionally
*
* Threading:
* - Uses a daemon thread pool named "testcontainers-lifecycle-[counter]"
* - Maximum pool size is unlimited (cached thread pool)
* - Threads are reused for efficiency
*
* @param startables stream of resources to start
* @return CompletableFuture<Void> that completes when all resources are started
* @throws CompletionException (wrapping underlying exception) if any resource fails to start
*/
public static CompletableFuture<Void> deepStart(Stream<? extends Startable> startables);
}Key Features:
getDependencies() declarationsUsage Examples:
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.container.Network;
import org.testcontainers.utility.DockerImageName;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
// Start containers with dependencies using varargs
GenericContainer<?> database = new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "secret");
GenericContainer<?> cache = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379);
GenericContainer<?> application = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.dependsOn(database, cache);
// Start all containers respecting dependencies
CompletableFuture<Void> started = Startables.deepStart(database, cache, application);
started.join(); // Wait for all to complete
System.out.println("All containers started");
// Access containers
String dbHost = database.getHost();
Integer dbPort = database.getMappedPort(5432);
String appHost = application.getHost();
Integer appPort = application.getMappedPort(8080);Using Collections:
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
List<GenericContainer<?>> services = new ArrayList<>();
// Create multiple service instances
for (int i = 0; i < 3; i++) {
GenericContainer<?> service = new GenericContainer<>(
DockerImageName.parse("myservice:latest")
).withExposedPorts(8080);
services.add(service);
}
// Start all services in parallel
CompletableFuture<Void> allStarted = Startables.deepStart(services);
allStarted.join();
System.out.println("All " + services.size() + " services started");Using Streams:
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.stream.IntStream;
// Create containers using stream and start
IntStream.range(0, 5)
.mapToObj(i -> new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379))
.collect(java.util.stream.Collectors.toList())
.stream()
.peek(container -> Startables.deepStart(container).join());Error Handling:
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
GenericContainer<?> badContainer = new GenericContainer<>(
DockerImageName.parse("nonexistent:latest")
);
GenericContainer<?> goodContainer = new GenericContainer<>(
DockerImageName.parse("redis:7.0")
);
CompletableFuture<Void> startup = Startables.deepStart(badContainer, goodContainer);
try {
startup.join();
} catch (Exception e) {
System.err.println("Failed to start containers: " + e.getCause());
e.getCause().printStackTrace();
}Complex Dependency Graph:
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
// Build a complex service topology
GenericContainer<?> database = new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "secret");
GenericContainer<?> cache = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379);
GenericContainer<?> messageQueue = new GenericContainer<>(DockerImageName.parse("rabbitmq:3-management"))
.withExposedPorts(5672, 15672);
GenericContainer<?> authService = new GenericContainer<>(DockerImageName.parse("auth-service:latest"))
.withExposedPorts(8080)
.dependsOn(database);
GenericContainer<?> apiService = new GenericContainer<>(DockerImageName.parse("api-service:latest"))
.withExposedPorts(8080)
.dependsOn(database, cache, authService);
GenericContainer<?> workerService = new GenericContainer<>(DockerImageName.parse("worker-service:latest"))
.dependsOn(messageQueue, database);
// Start entire system with optimal parallelization
Startables.deepStart(
database,
cache,
messageQueue,
authService,
apiService,
workerService
).join();
System.out.println("Entire microservices system started");
// Startup order with this graph:
// Parallel: database, cache, messageQueue
// Then: authService (depends on database)
// Parallel to authService: apiService waits for all three
// Parallel to authService: workerService (depends on messageQueue, database)
// Final: apiService starts after authService completesTest Integration:
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.junit.jupiter.api.BeforeAll;
import java.util.concurrent.ExecutionException;
@Testcontainers
public class IntegrationTest {
@Container
private static final GenericContainer<?> database =
new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "secret");
@Container
private static final GenericContainer<?> application =
new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.dependsOn(database);
// Note: @Testcontainers annotation handles startup automatically
// But you can also use deepStart for manual control
@BeforeAll
public static void setupWithManualStart() throws ExecutionException, InterruptedException {
// If not using @Testcontainers, manually start with deepStart
Startables.deepStart(database, application).get();
}
// Tests now run with both containers available
}Interface for containers and resources that need to react to test lifecycle events. This enables containers to perform test-specific initialization and cleanup, or capture diagnostics when tests fail.
/**
* Interface for containers that are aware of and react to test lifecycle events.
* Implementations can hook into test start and completion events to perform
* test-specific initialization, cleanup, or diagnostic collection.
*
* Implementations of this interface will have their methods called by compatible
* test integrations (JUnit 4, JUnit 5, TestNG, etc.).
*/
public interface TestLifecycleAware {
/**
* Called before a test starts.
* Default implementation does nothing.
*
* Typical uses:
* - Setup test-specific state in the container
* - Clear caches or reset database state
* - Configure per-test logging or monitoring
* - Initialize test fixtures
*
* @param description metadata about the test about to run
*/
default void beforeTest(TestDescription description) {}
/**
* Called after a test completes, whether success or failure.
* Default implementation does nothing.
*
* Typical uses:
* - Capture diagnostic logs if test failed
* - Verify state assertions for testing the container itself
* - Save test artifacts or results
* - Cleanup test-specific resources
* - Perform health checks
*
* @param description metadata about the completed test
* @param throwable optional exception if the test failed
* Present (non-empty) if test threw an exception
* Empty (Optional.empty()) if test passed
*/
default void afterTest(TestDescription description, Optional<Throwable> throwable) {}
}Implemented By:
GenericContainerUsage Examples:
import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.Optional;
/**
* PostgreSQL container that resets state between tests
*/
public class TestAwarePostgreSQLContainer
extends GenericContainer<TestAwarePostgreSQLContainer>
implements TestLifecycleAware {
public TestAwarePostgreSQLContainer() {
super(DockerImageName.parse("postgres:15"));
}
@Override
protected void configure() {
withExposedPorts(5432);
withEnv("POSTGRES_PASSWORD", "test");
}
@Override
public void beforeTest(TestDescription description) {
try {
// Clear all data before each test
execInContainer("psql", "-U", "postgres", "-c",
"DROP SCHEMA public CASCADE; CREATE SCHEMA public;");
System.out.println("Database reset for test: " + description.getTestId());
} catch (Exception e) {
throw new RuntimeException("Failed to reset database", e);
}
}
@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
if (throwable.isPresent()) {
try {
// Capture database state for failed tests
String logs = execInContainer("psql", "-U", "postgres", "-c",
"SELECT * FROM information_schema.tables;")
.getStdout();
System.out.println("Database tables on test failure (" +
description.getTestId() + "):\n" + logs);
} catch (Exception e) {
System.err.println("Failed to capture diagnostics: " + e.getMessage());
}
}
}
public String getJdbcUrl() {
return String.format("jdbc:postgresql://%s:%d/postgres",
getHost(),
getMappedPort(5432));
}
}
// Usage in tests
class MyDatabaseTest {
static final TestAwarePostgreSQLContainer db =
new TestAwarePostgreSQLContainer();
@BeforeAll
static void setup() {
db.start();
}
// beforeTest() and afterTest() are called automatically by test framework
// if integrated with Testcontainers
}Monitoring Container Example:
import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.Optional;
import java.time.Instant;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Prometheus monitoring container that captures metrics per test
*/
public class PrometheusTestContainer
extends GenericContainer<PrometheusTestContainer>
implements TestLifecycleAware {
private final Path metricsDir;
private Instant testStartTime;
public PrometheusTestContainer(Path metricsDir) {
super(DockerImageName.parse("prom/prometheus:latest"));
this.metricsDir = metricsDir;
}
@Override
protected void configure() {
withExposedPorts(9090);
}
@Override
public void beforeTest(TestDescription description) {
testStartTime = Instant.now();
System.out.println("Starting metrics collection for: " +
description.getTestId());
}
@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
try {
// Query metrics from Prometheus
String metrics = execInContainer("wget", "-O", "-",
"http://localhost:9090/api/v1/query?query=up")
.getStdout();
// Save to test-specific file
String filename = description.getFilesystemFriendlyName() + ".metrics";
Path metricsFile = metricsDir.resolve(filename);
Files.createDirectories(metricsFile.getParent());
Files.writeString(metricsFile, metrics);
System.out.println("Metrics saved to: " + metricsFile);
if (throwable.isPresent()) {
System.out.println("Test " + description.getTestId() +
" FAILED. Metrics available at: " + metricsFile);
}
} catch (Exception e) {
System.err.println("Failed to collect metrics: " + e.getMessage());
}
}
public String getPrometheusUrl() {
return String.format("http://%s:%d",
getHost(),
getMappedPort(9090));
}
}
// Usage
class PerformanceTest {
static final Path METRICS_DIR = Paths.get("target/test-metrics");
static final PrometheusTestContainer prometheus =
new PrometheusTestContainer(METRICS_DIR);
@BeforeAll
static void setup() {
prometheus.start();
}
// beforeTest() captures start time
// afterTest() saves metrics and diagnostics
}Logging Container Example:
import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.util.Optional;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Application container that saves logs per test
*/
public class LogCapturingContainer
extends GenericContainer<LogCapturingContainer>
implements TestLifecycleAware {
private final Path logsDir;
public LogCapturingContainer(Path logsDir) {
super(DockerImageName.parse("myapp:latest"));
this.logsDir = logsDir;
}
@Override
protected void configure() {
withExposedPorts(8080);
}
@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
try {
String logs = getLogs();
String filename = description.getFilesystemFriendlyName() + ".log";
Path logFile = logsDir.resolve(filename);
Files.createDirectories(logFile.getParent());
Files.writeString(logFile, logs);
if (throwable.isPresent()) {
System.out.println("Test failed. Full logs saved to: " + logFile);
// Log last 50 lines to console for quick debugging
String[] lines = logs.split("\n");
int start = Math.max(0, lines.length - 50);
System.out.println("\nLast 50 lines of container logs:");
for (int i = start; i < lines.length; i++) {
System.out.println(lines[i]);
}
}
} catch (Exception e) {
System.err.println("Failed to capture logs: " + e.getMessage());
}
}
}Metadata interface providing information about the test being executed. Passed to TestLifecycleAware callbacks.
/**
* Describes a test being executed.
* Provides metadata that can be used to customize container behavior per test.
*/
public interface TestDescription {
/**
* Get the unique test identifier.
* This is typically the fully qualified method name: "package.Class.method"
* or sometimes includes parameters for parameterized tests.
*
* Suitable for:
* - Logging and diagnostics
* - Creating test-specific resources or tags
* - Identifying tests in container state
*
* @return unique test identifier string
*/
String getTestId();
/**
* Get a filesystem-friendly name for the test.
* This is a sanitized version of the test ID with special characters removed/replaced.
* Safe to use as a filename or directory name.
*
* Conversions applied:
* - Colons, slashes, and other special characters replaced with underscores
* - Path separators normalized to underscores
* - Length may be limited depending on filesystem restrictions
*
* Examples:
* - "com.example.MyTest.testExample" -> "com_example_MyTest_testExample"
* - "com.example.ParamTest.testWith[1]" -> "com_example_ParamTest_testWith_1_"
*
* Suitable for:
* - Creating test-specific log files
* - Creating test-specific directories
* - Creating test-specific artifacts
*
* @return filesystem-safe test name string
*/
String getFilesystemFriendlyName();
}Typical Implementation in Test Frameworks:
import org.testcontainers.lifecycle.TestDescription;
import java.lang.reflect.Method;
/**
* Example implementation for JUnit 5
*/
public class JUnit5TestDescription implements TestDescription {
private final String testId;
private final String filesystemFriendlyName;
public JUnit5TestDescription(Class<?> testClass, Method testMethod) {
this.testId = testClass.getName() + "." + testMethod.getName();
this.filesystemFriendlyName = sanitizeForFilesystem(testId);
}
@Override
public String getTestId() {
return testId;
}
@Override
public String getFilesystemFriendlyName() {
return filesystemFriendlyName;
}
private static String sanitizeForFilesystem(String testId) {
return testId
.replaceAll("[:\\\\/<>|?*]", "_") // Replace special chars
.replaceAll("\\[", "_") // Replace bracket
.replaceAll("\\]", "_")
.replaceAll("\\s+", "_"); // Replace spaces
}
}Usage in TestLifecycleAware Implementations:
import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.containers.GenericContainer;
import java.util.Optional;
import java.nio.file.Paths;
public class DiagnosticContainer
extends GenericContainer<DiagnosticContainer>
implements TestLifecycleAware {
@Override
public void beforeTest(TestDescription description) {
String testId = description.getTestId();
String friendlyName = description.getFilesystemFriendlyName();
System.out.println("Starting test: " + testId);
System.out.println(" Artifact directory: " + friendlyName);
// Create test-specific directories using friendly name
java.nio.file.Path artifactDir = Paths.get("target", "artifacts", friendlyName);
try {
java.nio.file.Files.createDirectories(artifactDir);
} catch (Exception e) {
System.err.println("Failed to create artifact directory: " + e);
}
}
@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
String testId = description.getTestId();
String friendlyName = description.getFilesystemFriendlyName();
if (throwable.isPresent()) {
System.out.println("Test FAILED: " + testId);
System.out.println(" Diagnostics in: " + friendlyName);
// Save detailed diagnostics
try {
String logs = getLogs();
java.nio.file.Path logFile = Paths.get(
"target", "artifacts", friendlyName, "container.log");
java.nio.file.Files.writeString(logFile, logs);
} catch (Exception e) {
System.err.println("Failed to save logs: " + e);
}
} else {
System.out.println("Test PASSED: " + testId);
}
}
}import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.lifecycle.Startables;
import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.utility.DockerImageName;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
/**
* Complete example of lifecycle management in a test
*/
public class LifecycleManagementExample {
/**
* Custom container with lifecycle awareness
*/
public static class ApplicationContainer
extends GenericContainer<ApplicationContainer>
implements TestLifecycleAware {
private final Path diagnosticsDir;
public ApplicationContainer(Path diagnosticsDir) {
super(DockerImageName.parse("myapp:latest"));
this.diagnosticsDir = diagnosticsDir;
}
@Override
protected void configure() {
withExposedPorts(8080)
.withEnv("LOG_LEVEL", "INFO");
}
@Override
public void beforeTest(TestDescription description) {
System.out.println("Setting up test: " + description.getTestId());
try {
// Reset application state for each test
execInContainer("curl", "-X", "POST", "http://localhost:8080/api/reset");
} catch (Exception e) {
throw new RuntimeException("Failed to reset application", e);
}
}
@Override
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
try {
String logs = getLogs();
Path logFile = diagnosticsDir.resolve(
description.getFilesystemFriendlyName() + ".log");
Files.createDirectories(logFile.getParent());
Files.writeString(logFile, logs);
if (throwable.isPresent()) {
System.out.println("Test failed. Logs: " + logFile);
}
} catch (Exception e) {
System.err.println("Failed to save diagnostics: " + e);
}
}
public String getAppUrl() {
return String.format("http://%s:%d", getHost(), getMappedPort(8080));
}
}
// Setup containers with dependencies
static Network network = Network.newNetwork();
static GenericContainer<?> database = new GenericContainer<>(
DockerImageName.parse("postgres:15")
)
.withNetwork(network)
.withNetworkAliases("postgres")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "test");
static GenericContainer<?> cache = new GenericContainer<>(
DockerImageName.parse("redis:7.0")
)
.withNetwork(network)
.withNetworkAliases("redis")
.withExposedPorts(6379);
static ApplicationContainer app = new ApplicationContainer(
Paths.get("target", "test-diagnostics")
)
.withNetwork(network)
.withEnv("DB_HOST", "postgres")
.withEnv("CACHE_HOST", "redis")
.withExposedPorts(8080)
.dependsOn(database, cache);
@BeforeAll
static void startAllContainers() {
// Start all containers with optimized parallelization
Startables.deepStart(database, cache, app).join();
System.out.println("All containers started");
}
@Test
void testApplicationFunctionality() {
String appUrl = app.getAppUrl();
System.out.println("Testing application at: " + appUrl);
// Test application
// beforeTest() callback is automatically called
// afterTest() callback is automatically called on completion
}
@Test
void testWithDatabase() {
String dbHost = database.getHost();
Integer dbPort = database.getMappedPort(5432);
System.out.println("Database at: " + dbHost + ":" + dbPort);
// Test with database
}
@Test
void testWithCache() {
String cacheHost = cache.getHost();
Integer cachePort = cache.getMappedPort(6379);
System.out.println("Cache at: " + cacheHost + ":" + cachePort);
// Test with cache
}
}The Testcontainers lifecycle management APIs provide:
These APIs enable: