| name | Testcontainers - DockerComposeEnvironment with Jest |
| description | Guide for using testcontainers DockerComposeEnvironment to manage infrastructure in Jest e2e tests. Covers globalSetup/globalTeardown integration, wait strategies, container connection info extraction, TypeScript usage, and Minio-specific setup. |
Testcontainers - DockerComposeEnvironment with Jest
Overview
testcontainers provides programmatic Docker container lifecycle management for tests. This skill focuses on using DockerComposeEnvironment to start the project's docker-compose.yaml services (including Minio) inside Jest e2e tests via globalSetup and globalTeardown. The environment starts once before all tests and tears down after, making infrastructure available for the entire test run.
Key Concepts
- DockerComposeEnvironment: Starts services defined in a docker-compose file; supports wait strategies, partial startup, environment variable overrides
- globalSetup: Jest configuration option pointing to a module that exports
setup() and teardown() functions - runs once before all test files in a separate process
- globalTeardown: Jest configuration option pointing to a module that exports a teardown function - runs once after all test files
- Wait.forLogMessage(msg): Wait strategy that polls container logs until a regex or string appears
- Wait.forHealthCheck(): Wait strategy that uses the service's docker-compose healthcheck
- getMappedPort(port): Returns the host port mapped to the given container port
- getHost(): Returns the host address for the container (typically
localhost in most environments)
- environment.down(): Stops and removes all containers started by the environment
Documentation & References
Recommended Libraries & Tools
| Name | Purpose | Maturity | Notes |
|---|
testcontainers | Core container management | Stable (11.13.0) | Includes DockerComposeEnvironment |
Recommended Stack
Use testcontainers@11.13.0 with DockerComposeEnvironment pointing at the project's docker-compose.yaml. Configure Jest jest.e2e.config.js with globalSetup and globalTeardown paths.
Patterns & Best Practices
Pattern 1: globalSetup + globalTeardown with DockerComposeEnvironment
When to use: E2E tests that require real infrastructure (Minio, databases) started once for the entire test suite.
Critical constraint: globalSetup runs in a completely separate Node.js process from the test files. Variables set in globalSetup are NOT accessible in tests via normal module scope. Use process.env to pass connection info to tests.
import path from 'node:path'
import { DockerComposeEnvironment, Wait } from 'testcontainers'
import type { StartedDockerComposeEnvironment } from 'testcontainers'
let environment: StartedDockerComposeEnvironment
export async function setup(): Promise<void> {
const composeFilePath = path.resolve(__dirname, '..')
environment = await new DockerComposeEnvironment(composeFilePath, 'docker-compose.yaml')
.withWaitStrategy('minio-1', Wait.forHealthCheck())
.up(['minio'])
const minioContainer = environment.getContainer('minio-1')
process.env.S3_ENDPOINT = `http://${minioContainer.getHost()}:${minioContainer.getMappedPort(9000)}`
process.env.S3_ACCESS_KEY_ID = 'minioadmin'
process.env.S3_SECRET_ACCESS_KEY = 'minioadmin'
process.env.S3_BUCKET = 'analytics-data'
process.env.S3_REGION = 'us-east-1'
}
export async function teardown(): Promise<void> {
await environment?.down({ timeout: 10_000, removeVolumes: true })
}
Note on service container name: Docker Compose appends -1 (or the scale index) to the service name. A service named minio in docker-compose becomes minio-1 for getContainer('minio-1').
Pattern 2: Jest Config Integration
When to use: Configuring Jest e2e tests to use global setup.
const baseConfig = require('./jest.config.js')
module.exports = {
...baseConfig,
testRegex: '\\.e2e-spec\\.ts$',
globalSetup: '<rootDir>/test/e2e-global-setup.ts',
globalTeardown: '<rootDir>/test/e2e-global-teardown.ts',
setupFiles: [...(baseConfig.setupFiles ?? []), '<rootDir>/test/e2e-env-setup.ts'],
}
TypeScript globalSetup: Jest supports TypeScript globalSetup files via ts-jest transformer. Ensure ts-jest is configured in the base jest.config.js (it is in this project).
However, globalSetup / globalTeardown are loaded directly by Jest without the TypeScript transform. Options:
Option A: Separate setup and teardown files (simplest, avoids shared state):
Option B: Use ts-node for TypeScript globalSetup - configure ts-node in jest config or use @swc/jest:
Option C: Compile TS to JS first (most reliable for CI):
globalSetup: '<rootDir>/dist/test/e2e-global-setup.js',
Recommended for this project (ts-jest already configured): Use .ts files for globalSetup but ensure ts-node is registered or use the --require ts-node/register pattern. Alternatively, write globalSetup as a .js file wrapping a compiled .ts module.
Pattern 3: Wait Strategies
When to use: Services take time to initialize after docker start; without wait strategies tests may run before the service is ready.
new DockerComposeEnvironment(path, file)
.withWaitStrategy('minio-1', Wait.forLogMessage('API'))
.withWaitStrategy('minio-1', Wait.forHealthCheck())
.withWaitStrategy('service-1', Wait.forHttp('/health', 8080).forStatusCode(200))
Pattern 4: Starting Only Specific Services
When to use: Full docker-compose has many services but e2e tests only need Minio.
const environment = await new DockerComposeEnvironment(path, 'docker-compose.yaml')
.withWaitStrategy('minio-1', Wait.forHealthCheck())
.up(['minio'])
Note: Also starts any services that minio depends on (via depends_on).
Pattern 5: Environment Variable Overrides
When to use: Override docker-compose environment variables for test-specific configuration.
const environment = await new DockerComposeEnvironment(path, 'docker-compose.yaml')
.withEnvironment({ MINIO_ROOT_USER: 'testuser', MINIO_ROOT_PASSWORD: 'testpass' })
.up(['minio'])
Similar Implementations
Example 1: Redis Global Setup (Vitest)
Common Pitfalls & Solutions
| Issue | Impact | Solution |
|---|
| globalSetup variables not visible in test files | High | Use process.env to pass connection info; globalThis only visible in globalTeardown |
getContainer('minio') throws | High | Service name in compose is minio, but container name is minio-1 (with index) |
| TypeScript globalSetup not transformed by ts-jest | High | Write globalSetup in JS, or use ts-node/register, or compile TS first |
| Container not ready when tests run | High | Add .withWaitStrategy('minio-1', Wait.forHealthCheck()) |
| S3 bucket not pre-created when service starts | Medium | Add minio-init service in docker-compose with depends_on + condition: service_healthy |
| Timeout on environment.down() | Low | Pass { timeout: 10_000 } to down(); default may wait indefinitely |
Docker compose secrets in compose file | Medium | testcontainers may reject compose files with unsupported options; remove secrets or use override file |
Recommendations
- Use
Wait.forHealthCheck(): Requires a healthcheck in the docker-compose service definition. This is the most reliable wait strategy for Minio.
- Pass connection info via
process.env: This is the only way to share data from globalSetup to test files (they run in separate processes).
- Start only needed services with
.up(['service']): Avoid starting the full stack in e2e tests to reduce startup time.
- Store environment in module-level variable: The
globalSetup and globalTeardown can share state if exported from the same module - reference the environment instance for teardown.
Implementation Guidance
Jest e2e Config Update
const baseConfig = require('./jest.config.js')
module.exports = {
...baseConfig,
testRegex: '\\.e2e-spec\\.ts$',
globalSetup: '<rootDir>/test/e2e-global-setup.ts',
setupFiles: [...(baseConfig.setupFiles ?? []), '<rootDir>/test/e2e-env-setup.ts'],
}
Integration Points
globalSetup starts Minio before any test file runs
process.env.S3_ENDPOINT (and other vars) set by globalSetup are read by e2e-env-setup.ts via configify
teardown (exported from globalSetup module or separate file) stops containers after all tests
- NestJS app in tests reads config from
process.env populated by the setup chain
Code Examples
Example 1: Complete e2e-global-setup.ts
import path from 'node:path'
import { DockerComposeEnvironment, Wait } from 'testcontainers'
import type { StartedDockerComposeEnvironment } from 'testcontainers'
let environment: StartedDockerComposeEnvironment | undefined
export async function setup(): Promise<void> {
const composeFilePath = path.resolve(__dirname, '..')
environment = await new DockerComposeEnvironment(composeFilePath, 'docker-compose.yaml')
.withWaitStrategy('minio-1', Wait.forHealthCheck())
.up(['minio', 'minio-init'])
const minioContainer = environment.getContainer('minio-1')
const minioHost = minioContainer.getHost()
const minioPort = minioContainer.getMappedPort(9000)
process.env.S3_ENDPOINT = `http://${minioHost}:${minioPort}`
process.env.S3_ACCESS_KEY_ID = 'minioadmin'
process.env.S3_SECRET_ACCESS_KEY = 'minioadmin'
process.env.S3_BUCKET = 'analytics-data'
process.env.S3_REGION = 'us-east-1'
}
export async function teardown(): Promise<void> {
await environment?.down({ timeout: 10_000, removeVolumes: true })
}
Example 2: Using Container Info in Tests
describe('AnalyticsInterceptor (e2e)', () => {
it('persists analytics data to S3', async () => {
const s3Client = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
})
})
})
Sources & Verification
Changelog
| Date | Changes |
|---|
| 2026-03-27 | Initial creation for task: add-decision-data-saving |