Introduction to Test Lifecycle Management
Welcome back, fellow explorers of robust testing! In previous chapters, we learned the magic of spinning up disposable containers to test our applications with real dependencies. We’ve seen how Testcontainers simplifies setting up databases like PostgreSQL and message brokers like Kafka, freeing us from the shackles of mocks and in-memory fakes.
But here’s a thought: What happens to these containers after our tests run? And what if starting a new container for every single test method slows down our test suite to a crawl? This is where Testcontainers’ lifecycle management truly shines.
In this chapter, we’ll dive deep into controlling when Testcontainers start and stop your Docker containers. You’ll learn:
- Why managing container lifecycles is crucial for both performance and resource efficiency.
- The different strategies Testcontainers offers for container scoping (per-test, per-class/module, and even reusable containers).
- How to implement these strategies using language-specific hooks in Java (JUnit 5), Python (Pytest), and JavaScript/TypeScript (Jest/Mocha).
- Best practices for ensuring your containers are cleaned up properly, preventing resource leaks.
By the end of this chapter, you’ll be able to design highly efficient and reliable integration tests, ensuring your test suite runs fast and cleans up after itself like a good citizen.
Core Concepts: Why Lifecycle Matters
Imagine you have 100 integration tests, and each one needs a PostgreSQL database. If every test method starts a brand new PostgreSQL container, downloads the image, and waits for it to initialize, your test suite could take a very, very long time. Not only that, but constantly creating and destroying containers can consume significant system resources. This is the core problem that robust lifecycle management solves.
The Default: Per-Test Container Lifecycle
By default, when you define a GenericContainer (or a specialized one like PostgreSQLContainer) and use it directly within a single test method, Testcontainers treats its lifecycle as “per-test.”
What it means:
- Before the test method runs: Testcontainers starts the container.
- After the test method finishes: Testcontainers stops and removes the container.
Pros:
- Isolation: Each test gets a fresh, isolated environment. No test can inadvertently affect the state of another. This is the “gold standard” for deterministic tests.
- Simplicity: Often the easiest to set up for a small number of tests.
Cons:
- Performance Overhead: The startup time for Docker containers can be significant, especially for complex images or those downloaded for the first time.
- Resource Consumption: Constant starts and stops might tie up Docker daemon resources.
While excellent for isolation, the performance hit often necessitates a more optimized approach for larger test suites.
Beyond Per-Test: Shared Container Lifecycles
Testcontainers provides mechanisms to share a single container instance across multiple test methods or even multiple test classes. This significantly reduces startup overhead.
Strategy 1: Per-Class / Per-Module Scope
This is a common and often recommended strategy. A single container instance is started once for an entire test class (or a module/file in Python/Node.js) and then stopped after all tests in that scope have completed.
How it works:
- Before any test in the class/module runs: Testcontainers starts the container.
- All tests in that class/module run against the same container instance.
- After all tests in the class/module finish: Testcontainers stops and removes the container.
Pros:
- Performance Boost: Significantly faster than per-test, as container startup occurs only once.
- Good Isolation (with care): While not as strictly isolated as per-test, you can manage state within the tests themselves (e.g., clear database tables between tests) to maintain a good level of isolation.
Cons:
- State Management: If tests modify the container’s state (e.g., insert data into a database), subsequent tests using the same container instance might be affected. You’ll need strategies to reset the state between tests.
Strategy 2: Reusable Containers (Advanced)
For truly massive test suites or scenarios where container initialization is exceptionally slow, Testcontainers offers a “reusable” strategy. Here, containers are started once and kept running across multiple test runs (even across different JVMs/processes) or during local development for faster iteration.
How it works:
- You mark a container as reusable.
- Testcontainers checks if a compatible container is already running. If so, it reuses it.
- If not, it starts a new one and keeps it running after tests complete, tagged for future reuse.
- Manual cleanup might be required eventually or rely on an explicit “shutdown all reusable containers” command.
Pros:
- Maximum Performance: Minimal startup overhead, as containers might already be running.
- Fast Development Feedback: Great for local development loops.
Cons:
- Risk of State Leaks: The highest risk of state leakage between test runs. Requires extremely diligent state cleanup after each test.
- Explicit Cleanup: You need to explicitly manage when these containers are finally shut down. Not ideal for CI/CD environments where a clean slate is usually preferred.
- Configuration: Requires more specific configuration to enable and manage.
Cleanup Orchestration with Ryuk (and general mechanisms)
Regardless of the lifecycle strategy, Testcontainers is designed to clean up after itself. When you run Testcontainers-enabled tests, a tiny helper container named ryuk (or a similar internal mechanism in non-JVM languages) is typically started.
How Ryuk (and similar) works:
ryukmonitors the Docker daemon for containers started by Testcontainers.- If your test process terminates unexpectedly (e.g., your IDE crashes, or the JVM exits prematurely),
ryukacts as a watchdog. - Upon detecting that the Testcontainers client that started a container is no longer active,
ryukautomatically stops and removes those orphaned containers.
This ensures that even if your tests crash, you won’t be left with a graveyard of zombie Docker containers consuming resources. It’s a fantastic safety net!
Visualizing Container Lifecycles
Let’s use a simple flowchart to illustrate the difference between per-test and per-class lifecycles.
As you can see, in the “Per-Class” model, the container startup and shutdown happen only twice: once at the beginning of the class and once at the end. In contrast, for “Per-Test”, it happens for each test. That’s a huge difference!
Now, let’s explore how to implement these lifecycle strategies in our code across different programming languages.
Step-by-Step Implementation: Language-Specific Hooks
We’ll start by revisiting a basic PostgreSQL container and then apply per-class/module lifecycle management using each language’s testing framework hooks.
Java (JUnit 5 + Testcontainers)
For Java and JUnit 5, Testcontainers integrates beautifully with the @Container annotation and standard JUnit lifecycle annotations (@BeforeAll, @AfterAll). We’ll use Testcontainers Java version 1.19.x which is stable and widely adopted as of early 2026.
Setup (Review from previous chapters)
Ensure you have the following dependencies in your pom.xml (Maven) or build.gradle (Gradle):
<!-- Maven -->
<dependencies>
<!-- Testcontainers core -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.4</version> <!-- Use the latest 1.19.x for stability -->
<scope>test</scope>
</dependency>
<!-- Specific database module (e.g., PostgreSQL) -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.4</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Jupiter API -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version> <!-- Current stable for early 2026 -->
<scope>test</scope>
</dependency>
<!-- JUnit 5 Jupiter Engine (for running tests) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
Per-Class Lifecycle Example (Java)
To share a container across all tests in a class, declare the Testcontainers instance as a static field and annotate it with @Container.
Create a file named PostgreSQLLifecycleTest.java:
// src/test/java/com/example/PostgreSQLLifecycleTest.java
package com.example;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@Testcontainers // This annotation enables Testcontainers integration with JUnit 5
public class PostgreSQLLifecycleTest {
// 1. Declare the container as a static field and annotate with @Container
// Testcontainers will manage its lifecycle: start BEFORE all tests, stop AFTER all tests.
@Container
public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16.2")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
private Connection connection;
@BeforeAll // This JUnit hook runs once before ANY test method in this class.
static void setupClass() {
// We can assert the container is running and get connection details here.
System.out.println("Container started: " + postgres.getJdbcUrl());
assertNotNull(postgres.getJdbcUrl());
}
@BeforeEach // This JUnit hook runs before EACH test method.
void setup() throws SQLException {
// 2. Obtain a connection to the SAME running container for each test.
connection = DriverManager.getConnection(
postgres.getJdbcUrl(),
postgres.getUsername(),
postgres.getPassword()
);
// Important: Clean up state before each test to maintain isolation!
try (Statement stmt = connection.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS messages;"); // Clean up previous test's data
stmt.execute("CREATE TABLE messages (id SERIAL PRIMARY KEY, text VARCHAR(255));");
}
}
@Test
void testMessageInsertion() throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("INSERT INTO messages (text) VALUES ('Hello Testcontainers!');");
ResultSet rs = stmt.executeQuery("SELECT text FROM messages WHERE id = 1;");
rs.next();
assertEquals("Hello Testcontainers!", rs.getString("text"));
}
}
@Test
void testMessageCount() throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("INSERT INTO messages (text) VALUES ('First message');");
stmt.execute("INSERT INTO messages (text) VALUES ('Second message');");
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM messages;");
rs.next();
assertEquals(2, rs.getInt(1));
}
}
@AfterAll // This JUnit hook runs once after ALL test methods in this class have finished.
static void teardownClass() {
System.out.println("All tests in class finished. Testcontainers will now stop the container.");
// Testcontainers automatically stops 'postgres' because it's a static @Container.
// No manual 'postgres.stop()' needed here for lifecycle management.
}
}
Explanation:
@Testcontainers: This annotation is crucial. It activates the Testcontainers JUnit 5 extension, enabling it to detect and manage@Containerfields.@Container public static PostgreSQLContainer<?> postgres: By making the containerstaticand adding@Container, Testcontainers knows to start this container once before any tests inPostgreSQLLifecycleTestrun, and stop it once after all tests in this class complete.@BeforeAlland@AfterAll: These are standard JUnit 5 lifecycle hooks. We use@BeforeAllto log that the container has started (it will already be running by the time this hook executes).@AfterAllsimply serves as an observation point; Testcontainers handles thestop()call automatically.@BeforeEach: This hook runs before each test method. It’s vital here for managing state. We drop and recreate themessagestable to ensure each test starts with a clean slate, even though they share the same database instance. This is a common pattern for maintaining isolation within a shared container.
Python (Pytest + pytest-testcontainers)
For Python, we’ll leverage pytest’s powerful fixture system combined with the pytest-testcontainers library. We’ll use Testcontainers Python version 4.14.1, which was released on 2026-01-31.
Setup
Install the necessary libraries:
pip install pytest "testcontainers[postgresql]==4.14.1" pytest-testcontainers==0.3.0
(Note: pytest-testcontainers is a helper library for pytest integration. The version 0.3.0 is current as of early 2026, but always check PyPI for the latest stable release.)
Per-Module / Per-Session Lifecycle Example (Python)
Pytest fixtures allow you to define setup and teardown code with various scopes (function, class, module, session). For sharing across multiple tests in a file or across the entire test session, we’ll use module or session scope.
Create a file named test_postgresql_lifecycle.py:
# tests/test_postgresql_lifecycle.py
import pytest
from testcontainers.postgres import PostgreSQLContainer
import psycopg2 # A common PostgreSQL client library
# 1. Define a pytest fixture for the PostgreSQL container with 'module' scope.
# This means the container starts once before any test in this module runs
# and stops after all tests in this module are done.
@pytest.fixture(scope="module")
def postgres_container():
print("\n--- Starting PostgreSQL Container (module scope) ---")
with PostgreSQLContainer("postgres:16.2") \
.with_database_name("testdb") \
.with_user("testuser") \
.with_password("testpass") as postgres:
postgres.start()
yield postgres # The container is available here for tests
print("--- Stopping PostgreSQL Container (module scope) ---")
@pytest.fixture(scope="function")
def db_connection(postgres_container):
"""
Fixture to provide a database connection for each test function,
and ensure a clean state between tests.
"""
conn = psycopg2.connect(
host=postgres_container.get_container_host_ip(),
port=postgres_container.get_exposed_port(5432),
database=postgres_container.database_name,
user=postgres_container.username,
password=postgres_container.password
)
# 2. Clean up state before each test function
with conn.cursor() as cur:
cur.execute("DROP TABLE IF EXISTS messages;")
cur.execute("CREATE TABLE messages (id SERIAL PRIMARY KEY, text VARCHAR(255));")
conn.commit()
yield conn # The connection is available here for the test
conn.close() # Close the connection after each test
def test_message_insertion(db_connection):
with db_connection.cursor() as cur:
cur.execute("INSERT INTO messages (text) VALUES ('Hello Testcontainers from Python!');")
db_connection.commit()
cur.execute("SELECT text FROM messages WHERE id = 1;")
result = cur.fetchone()
assert result[0] == "Hello Testcontainers from Python!"
def test_message_count(db_connection):
with db_connection.cursor() as cur:
cur.execute("INSERT INTO messages (text) VALUES ('First Python msg');")
cur.execute("INSERT INTO messages (text) VALUES ('Second Python msg');")
db_connection.commit()
cur.execute("SELECT COUNT(*) FROM messages;")
result = cur.fetchone()
assert result[0] == 2
Explanation:
@pytest.fixture(scope="module"): This decorator makespostgres_containera pytest fixture. Settingscope="module"means Testcontainers will start this container once when the test module (test_postgresql_lifecycle.py) is loaded and tear it down after all tests in that module have run.with PostgreSQLContainer(...) as postgres: postgres.start() yield postgres: This is the core of the fixture. Thewithstatement ensures proper resource management.postgres.start()explicitly starts the container.yield postgrespasses the container instance to any test or other fixture that requests it. Code afteryieldwill run as teardown logic when the fixture’s scope ends.@pytest.fixture(scope="function") def db_connection(postgres_container): This fixture depends onpostgres_container. It establishes a connection and, crucially, performsDROP TABLEandCREATE TABLEoperations before each test function (scope="function"). This ensures a clean database state for every test, even though they share the same underlying container.yield connandconn.close(): The connection is provided to the test function, and then closed once the test completes.
To run these tests, navigate to your project’s root in the terminal and execute: pytest -s tests/test_postgresql_lifecycle.py (the -s flag shows print statements).
JavaScript/TypeScript (Node.js with Jest/Mocha + @testcontainers/testcontainers)
For Node.js environments, Testcontainers provides an idiomatic API. We’ll use @testcontainers/testcontainers version 10.x.x as of early 2026. This example will use Jest, but the pattern is similar for Mocha or other frameworks with beforeAll/afterAll hooks.
Setup
Create a new Node.js project:
mkdir node-lifecycle-tests && cd node-lifecycle-tests
npm init -y
npm install --save-dev jest @types/jest typescript ts-node testcontainers postgres testcontainers-registry @testcontainers/postgresql
testcontainers-registry is useful for providing an easy way to get commonly used containers. postgres is the Node.js client library for PostgreSQL.
Add a tsconfig.json for TypeScript:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
Add a jest.config.js for Jest:
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
Per-Suite Lifecycle Example (JavaScript/TypeScript)
Node.js test runners like Jest and Mocha provide beforeAll and afterAll hooks to manage setup and teardown for an entire test suite (usually a single test file).
Create a file named postgresql-lifecycle.test.ts:
// src/tests/postgresql-lifecycle.test.ts
import { PostgreSQLContainer } from '@testcontainers/postgresql';
import { Client } from 'pg'; // Node.js PostgreSQL client
import { GenericContainer } from 'testcontainers'; // For GenericContainer if needed
describe('PostgreSQL Lifecycle Tests', () => {
let postgresContainer: PostgreSQLContainer;
let client: Client;
// 1. beforeAll hook: Runs once before all tests in this describe block.
beforeAll(async () => {
console.log('--- Starting PostgreSQL Container (suite scope) ---');
postgresContainer = await new PostgreSQLContainer('postgres:16.2')
.withDatabaseName('testdb')
.withUsername('testuser')
.withPassword('testpass')
.start();
// 2. Establish a connection for the suite.
client = new Client({
host: postgresContainer.getHost(),
port: postgresContainer.getMappedPort(5432),
database: postgresContainer.getDatabaseName(),
user: postgresContainer.getUsername(),
password: postgresContainer.getPassword(),
});
await client.connect();
console.log(`Connected to PostgreSQL at: ${postgresContainer.getHost()}:${postgresContainer.getMappedPort(5432)}`);
}, 60 * 1000); // Set a higher timeout for container startup (e.g., 60 seconds)
// beforeEach hook: Runs before each individual test.
beforeEach(async () => {
// 3. Clean up state before each test to ensure isolation.
await client.query('DROP TABLE IF EXISTS messages;');
await client.query('CREATE TABLE messages (id SERIAL PRIMARY KEY, text VARCHAR(255));');
});
test('should insert a message into the database', async () => {
await client.query("INSERT INTO messages (text) VALUES ('Hello Testcontainers from Node.js!');");
const res = await client.query('SELECT text FROM messages WHERE id = 1;');
expect(res.rows[0].text).toBe('Hello Testcontainers from Node.js!');
});
test('should correctly count messages', async () => {
await client.query("INSERT INTO messages (text) VALUES ('First Node.js msg');");
await client.query("INSERT INTO messages (text) VALUES ('Second Node.js msg');");
const res = await client.query('SELECT COUNT(*) FROM messages;');
expect(parseInt(res.rows[0].count)).toBe(2);
});
// afterAll hook: Runs once after all tests in this describe block are finished.
afterAll(async () => {
console.log('--- Stopping PostgreSQL Container (suite scope) ---');
await client.end(); // Close the database connection
await postgresContainer.stop(); // Stop the Testcontainers container
});
});
Explanation:
beforeAll(async () => { ... }): This Jest hook executes once before any of thetestblocks within thisdescribesuite. Weawaitthe container’sstart()method here.postgresContainer = await new PostgreSQLContainer(...): We create and start the container instance, storing it in a variable accessible throughout the suite.client = new Client(...): A PostgreSQL client connection is established using the running container’s details.beforeEach(async () => { ... }): Similar to Java’s@BeforeEachand Python’s function-scoped fixture, this hook cleans up the database state before each test, ensuring isolation.afterAll(async () => { ... }): This hook executes once after alltestblocks in thedescribesuite have completed. Crucially, weawaitbothclient.end()to close the database connection andpostgresContainer.stop()to gracefully shut down the Docker container.
To run these tests, execute: npx jest src/tests/postgresql-lifecycle.test.ts.
Mini-Challenge: Share a Redis Container
You’ve seen how to share a PostgreSQL container within a single test class or suite. Now, let’s apply this knowledge to a different dependency: Redis.
Challenge:
Create a new test file (e.g., RedisLifecycleTest.java, test_redis_lifecycle.py, or redis-lifecycle.test.ts) that:
- Spins up a Redis container (using
GenericContainer("redis:7.2.4")orRedisContainer). - Ensures the Redis container is started once for the entire test file/class and stopped afterward.
- Includes at least two test methods/functions/cases that connect to this same Redis instance and perform a simple
SETandGEToperation. - Crucially, ensure that the Redis database is cleared or flushed before each test to prevent state leakage between tests. (Hint: Look for a
FLUSHDBcommand or similar client method).
Hint:
- Java: Use
static @ContainerandJedisclient. Use@BeforeEachtojedis.flushDB(). - Python: Use a
moduleorsessionscopedpytestfixture withredisclient. Use afunctionscoped fixture to get the client andclient.flushdb()before yielding. - Node.js: Use
beforeAllandafterAllwith theioredisclient. UsebeforeEachtoclient.flushdb().
What to Observe/Learn:
- How consistent are the lifecycle management patterns across different languages?
- What’s the best way to ensure isolation (clean state) when sharing a container?
- How do you adapt the client connection details for a different type of container (e.g., Redis instead of PostgreSQL)?
Take your time, refer to the examples above, and consult Testcontainers’ official documentation if you get stuck. The goal is true understanding, not just copy-pasting!
Common Pitfalls & Troubleshooting
Even with Testcontainers’ powerful lifecycle management, you might encounter some bumps. Here are common pitfalls and how to address them:
Containers Not Shutting Down (Resource Leaks):
- Symptom: After tests run,
docker psstill shows Testcontainers-managed containers, or you get “port already in use” errors on subsequent runs. - Cause:
@Testcontainersannotation (Java) or properafterAll/yieldteardown (Python/Node.js) is missing or incorrectly implemented.- The test process terminated unexpectedly without giving Testcontainers a chance to clean up.
- Fix:
- Double-check that your
Testcontainersobject is correctly defined (e.g.,static @Containerin Java,yieldin Python fixtures,await container.stop()in Node.jsafterAll). - Trust
Ryuk! It’s designed for this. Ifryukitself isn’t running or gets blocked, you might need to manually stop containers (docker rm -f $(docker ps -aq --filter label=org.testcontainers.session-id)). - Ensure your test runner is correctly configured to run teardown hooks.
- Double-check that your
- Symptom: After tests run,
Slow Test Execution Due to Excessive Container Restarts:
- Symptom: Tests take a very long time to run, and you see “Container started” messages repeatedly.
- Cause: You’re accidentally using a “per-test” lifecycle when you intended a shared one. Forgetting
staticin Java, or usingfunctionscope for a fixture in Python whenmoduleorsessionwas intended. - Fix: Review your container declaration and lifecycle hooks. Ensure containers that can be shared are configured with class/module/suite scope. Remember to implement state cleanup (
@BeforeEach,beforeEach, function-scoped fixtures) if sharing.
State Leakage Between Shared Tests:
- Symptom: Tests are flaky, sometimes passing, sometimes failing, especially when run in different orders. One test’s data appears in another test.
- Cause: You’re sharing a container instance but not cleaning up the state (e.g., database contents) between individual test methods.
- Fix: Implement aggressive state cleanup. For databases, this often means dropping/recreating tables or truncating them in a
@BeforeEach,beforeEach, or function-scoped fixture. For message queues, it might mean purging queues.
“Container already started” or Port Conflicts with Reusable Containers:
- Symptom: When using reusable containers, tests fail because a port is already bound, or Testcontainers tries to start a new container even if one is running.
- Cause: This typically happens if
Testcontainers.enabled = trueis set for reusable containers in a CI/CD pipeline or if the reuse strategy is misconfigured. Reusable containers are often better for local development and require careful management. - Fix: For CI/CD, avoid
reuseunless you have a sophisticated strategy for unique container names/ports per job or are extremely diligent with cleanup. For local development, ensure you’re explicitly stopping reusable containers when you’re done or use unique test session IDs. Make sure to check the specifictestcontainers.propertiesor environment variables used for reuse.
Summary
Phew! You’ve just unlocked a critical skill in your Testcontainers journey: mastering container lifecycle management. This isn’t just about making your tests run; it’s about making them run well – fast, reliably, and without leaving a mess behind.
Here are the key takeaways from this chapter:
- Default Behavior: Containers are typically started and stopped for each individual test, ensuring maximum isolation but often at a performance cost.
- Shared Lifecycles for Performance:
- Per-Class/Per-Module/Per-Suite: The most common optimization, where a single container serves multiple tests within a logical group. This significantly reduces startup time.
- Reusable Containers: An advanced strategy for extreme performance and local development, where containers persist across test runs. This requires careful state management and explicit cleanup.
- Isolation vs. Performance: This is a key trade-off. Sharing containers improves performance but necessitates strategies (like resetting database state
beforeEachtest) to maintain test isolation and prevent flakiness. - Automatic Cleanup: Testcontainers uses mechanisms like
Ryukto automatically stop orphaned containers, acting as a crucial safety net even if your tests crash. - Language-Specific Hooks:
- Java (JUnit 5): Use
static @Containerfields along with@BeforeAlland@AfterAllfor class-scoped containers, and@BeforeEachfor per-test state cleanup. - Python (Pytest): Leverage fixtures with
scope="module"orscope="session"for shared containers, andscope="function"fixtures for per-test state management. - Node.js (Jest/Mocha): Utilize
beforeAllandafterAllhooks for suite-scoped containers, andbeforeEachfor per-test state cleanup.
- Java (JUnit 5): Use
You’re now equipped to make informed decisions about how your test environments are managed, leading to more efficient and trustworthy integration tests.
What’s next? In the following chapter, we’ll take these optimized test suites and learn how to seamlessly integrate them into your Continuous Integration/Continuous Delivery (CI/CD) pipelines, like GitHub Actions and GitLab CI, ensuring your code is always tested against real dependencies in a production-like environment!
References
- Testcontainers Official Documentation: JUnit 5 Integration
- Testcontainers Official Documentation: Python
- Testcontainers Official Documentation: Node.js
- JUnit 5 User Guide: Test Execution Lifecycle
- Pytest Documentation: Fixtures
- Jest Documentation: Setup and Teardown
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.