Design and manage test environments that are reliable, reproducible, and cost-effective. Covers environment provisioning, data isolation, ephemeral environments, and environment-as-code.
Test environment management is the practice of provisioning, configuring, maintaining, and orchestrating isolated, repeatable, and reproducible environments for testing software. It matters when tests must run consistently across development, staging, and production-like setups, especially in continuous integration pipelines, release trains, and feature-driven development.
Creating a test environment begins with defining the desired state: a set of services, data, configuration, and network topology. The goal is to start from a clean slate and end with an environment ready for testing, including all dependencies.
A typical environment consists of a web application, a database, a message broker, and a cache layer. Each component must be deployed and connected. The process starts with a declarative configuration, often in a docker-compose.yml file:
version: '3.8'
services:
web:
image: myapp:v1.2.3
ports:
- "8080:80"
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgresql://db:5432/myapp
- REDIS_URL=redis://redis:6379
db:
image: postgres:14
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=testuser
- POSTGRES_PASSWORD=testpass
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
The environment is provisioned using docker-compose up -d --build, which builds images, starts services, and waits for health checks. The healthcheck field in the YAML ensures services are ready before proceeding:
web:
image: myapp:v1.2.3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
After starting, docker-compose ps reports service statuses. A failing health check produces a docker-compose logs web output with:
web_1 | Error: Failed to connect to database: could not connect to server: Connection refused
web_1 | Is the server running on host "db" (172.18.0.2) and accepting TCP/IP connections on port 5432?
This error indicates the database service is not ready when the web app starts, even though depends_on was declared. The fix is to add start_period and interval to the healthcheck, ensuring the web service waits for the database to be fully up.
Environment variables are the glue between services and test data. They must be consistent across test runs and easily overridden.
Configuration is typically managed through a .env file:
DATABASE_URL=postgresql://testuser:testpass@db:5432/myapp
REDIS_URL=redis://redis:6379
TEST_MODE=true
LOG_LEVEL=debug
API_BASE_URL=http://web:80
This file is loaded at runtime via docker-compose up -e .env. A common mistake is assuming that environment values in docker-compose.yml override .env, but they do not: environment values are static, while .env is parsed and interpolated.
For example, setting DATABASE_URL=postgresql://testuser:${DB_PASSWORD}@db:5432/myapp in .env fails if DB_PASSWORD is not defined. The solution is to use env_file to load multiple files:
services:
web:
image: myapp:v1.2.3
env_file:
- .env
- .env.test
environment:
- LOG_LEVEL=info
The .env.test file may contain test-specific overrides:
LOG_LEVEL=debug
TEST_MODE=true
FEATURE_FLAGS=enable-new-ui,disable-analytics
When docker-compose up, the final environment for web includes all variables, with later files overwriting earlier ones. The env_file order matters: .env is loaded first, then .env.test, so variables in .env.test take precedence.
Test data must be consistent, reproducible, and resettable. It includes seed data, database snapshots, and mock APIs.
To initialize data, a seed.sql script is run against the database:
-- seed.sql
INSERT INTO users (id, name, email) VALUES
(1, 'Alice', '[email protected]'),
(2, 'Bob', '[email protected]');
INSERT INTO posts (id, user_id, title, body) VALUES
(1, 1, 'First Post', 'Hello world!');
This script is executed using psql -h db -U testuser -d myapp -f seed.sql. A common failure mode is missing PGPASSWORD in the environment, causing a psql error:
psql: could not connect to server: FATAL: password authentication failed for user "testuser"
The fix is to set PGPASSWORD=testpass in the environment or pass it via PGPASSWORD=testpass psql ....
For larger datasets, a database dump is used:
pg_dump -h db -U testuser -d myapp -f /data/dump.sql
This file is mounted into the db service and restored on startup:
db:
image: postgres:14
volumes:
- pgdata:/var/lib/postgresql/data
- ./data/dump.sql:/docker-entrypoint-initdb.d/dump.sql
The docker-entrypoint-initdb.d directory runs all .sql files on first startup, ensuring the database is seeded every time the container starts.
Each test environment must be versioned and tracked through its lifecycle: creation, usage, and cleanup.
A standard practice is to name environments by branch, commit, or release. For example, feature/user-profile-123 or ci-12345.
Environment state is versioned using git and docker-compose:
git checkout feature/user-profile-123
git rev-parse HEAD
# Output: 9a3e2b1c4f5d6e7a8b9c0d1e2f3a4b5c6d7e8f9a
# Tag the environment
docker-compose up -d --build
docker-compose run --rm web git rev-parse HEAD > .env.commit
This writes the current commit SHA to .env.commit, which is then used to tag the environment.
A cleanup.sh script removes unused environments:
#!/bin/bash
# cleanup.sh
for env in $(docker-compose ps -q); do
if ! docker-compose ps -q | grep -q "$env"; then
docker-compose down -v --remove-orphans
fi
done
# Remove dangling images
docker image prune -f
This script runs nightly or after CI jobs, ensuring environments are not left running indefinitely.
Test environments are reused across pipelines, but each pipeline may require different configurations.
A test-environment.yml file defines a test suite with environment setup:
name: E2E Tests
on:
pull_request:
branches: [ main ]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Start test environment
run: |
docker-compose up -d --build
sleep 30 # Wait for services to stabilize
- name: Run tests
run: |
curl -f http://localhost:8080/health
npm run test:e2e
- name: Stop environment
run: |
docker-compose down
This workflow starts the environment, waits 30 seconds (a critical but often underestimated step), checks health, runs tests, and shuts down the environment.
A subtle failure mode is test flakiness due to insufficient startup time. The curl -f http://localhost:8080/health may fail with:
curl: (7) Failed to connect to localhost port 8080: Connection refused
The fix is to wrap the health check in a loop:
#!/bin/bash
# wait-for-services.sh
for i in {1..30}; do
if curl -f http://localhost:8080/health; then
echo "Services ready"
exit 0
fi
echo "Waiting for services..."
sleep 2
done
echo "Timed out waiting for services"
exit 1
This script is run before npm run test:e2e and prevents flaky tests.
depends_on, the application starts before the database is ready. The solution is to use healthcheck on the database and depends_on with condition: service_healthy..env file is present but not used. The cause is missing env_file in docker-compose.yml or incorrect file path.pg_dump was not re-run or docker-entrypoint-initdb.d was not used. The fix is to add --no-logs to pg_dump and ensure .sql files are in the correct directory.docker-compose down --volumes --remove-orphans and add it to the cleanup script.docker-compose.yml. The error could not connect to server: Connection refused often points to a missing depends_on or networks.web service uses myapp:v1.2.3, but the db service expects myapp:v1.2.0. The fix is to use docker-compose build --pull and tag images with git rev-parse HEAD.config/test.json is mounted into the web service, but the application does not read it. The cause is incorrect path or missing config directory in the container.Test environment management is the foundation of reliable testing. It requires defining environments declaratively, provisioning them with tools like docker-compose, managing data and configuration, and orchestrating their lifecycle. The practice is not about having environments—it is about having the *right* environments, at the *right* time, with *consistent* data and *correct* configuration. The success of testing hinges on this discipline.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy