A properly configured test environment is critical for reliable, repeatable testing. This guide covers everything from basic setup to advanced containerization strategies, ensuring your test environment accurately reflects production while remaining isolated and manageable.
What is a Test Environment?
A test environment is a setup of software and hardware where testing teams execute test cases. It mimics production conditions while providing isolation for safe testing without affecting live systems.
Key Components
Infrastructure:
- Servers (physical or virtual)
- Network configuration
- Storage systems
- Load balancers
Software:
- Operating systems
- Application servers
- Databases
- Third-party integrations
- Monitoring tools
Data:
- Test databases
- Sample data sets
- Configuration files
- Environment variables
Access & Security:
- User accounts
- Permissions
- API keys
- SSL certificates
Types of Test Environments
1. Development Environment (DEV)
Used by developers for coding and initial testing.
# dev-environment.yml
environment: development
database:
host: localhost
port: 5432
name: myapp_dev
user: dev_user
cache:
provider: redis
host: localhost
port: 6379
api:
base_url: http://localhost:3000
debug_mode: true
log_level: DEBUG
features:
email_service: mock # Don't send real emails
payment_gateway: sandbox
cdn: local_storage
monitoring:
enabled: false # No monitoring in dev
2. Testing/QA Environment
Dedicated environment for QA team testing.
# qa-environment.yml
environment: qa
database (as discussed in [Continuous Testing in DevOps: Quality Gates and CI/CD Integration](/blog/continuous-testing-devops)):
host: qa-db.internal.company.com
port: 5432
name: myapp_qa
user: qa_user
pool_size: 10
cache:
provider: redis
(as discussed in [Grey Box Testing: Best of Both Worlds](/blog/grey-box-testing)) host: qa-redis.internal.company.com
port: 6379
cluster: true
api:
base_url: https://qa.myapp.com
debug_mode: false
log_level: INFO
features:
email_service: mailtrap # Email testing service
payment_gateway: stripe_test
cdn: qa_cdn
monitoring (as discussed in [Risk-Based Testing: Prioritizing Test Efforts for Maximum Impact](/blog/risk-based-testing)):
enabled: true
service: datadog
alerts: qa-team@company.com
performance:
rate_limit: 1000_per_minute
max_connections: 100
test_data:
auto_refresh: daily
anonymized: true
3. Staging Environment
Pre-production environment that mirrors production.
# staging-environment.yml
environment: staging
database:
host: staging-db.company.com
port: 5432
name: myapp_staging
user: staging_user
pool_size: 50
replication: true
cache:
provider: redis
host: staging-redis.company.com
port: 6379
cluster: true
persistence: true
api:
base_url: https://staging.myapp.com
debug_mode: false
log_level: WARNING
features:
email_service: sendgrid # Real email service
payment_gateway: stripe_test # Still test mode
cdn: cloudfront
monitoring:
enabled: true
service: datadog
alerts: ops-team@company.com
apm: true
performance:
rate_limit: 10000_per_minute
max_connections: 500
security:
ssl: true
firewall: true
vpc: staging-vpc
4. Production Environment
Live environment serving real users.
# production-environment.yml
environment: production
database:
host: prod-db.company.com
port: 5432
name: myapp_production
user: prod_user
pool_size: 100
replication: true
backups:
frequency: hourly
retention: 30_days
cache:
provider: redis
host: prod-redis.company.com
port: 6379
cluster: true
persistence: true
failover: automatic
api:
base_url: https://api.myapp.com
debug_mode: false
log_level: ERROR
features:
email_service: sendgrid
payment_gateway: stripe_live
cdn: cloudfront
monitoring:
enabled: true
service: datadog
alerts: oncall@company.com
apm: true
uptime_checks: true
performance:
rate_limit: 100000_per_minute
max_connections: 5000
auto_scaling: true
security:
ssl: true
firewall: true
vpc: production-vpc
waf: enabled
ddos_protection: true
Test Environment Setup Steps
Step 1: Infrastructure Provisioning
# Infrastructure as Code (Terraform)
# terraform/qa-environment.tf
# VPC Configuration
resource "aws_vpc" "qa_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "qa-vpc"
Environment = "qa"
}
}
# Subnet
resource "aws_subnet" "qa_subnet" {
vpc_id = aws_vpc.qa_vpc.id
cidr_block = "10.0.1.0/24"
tags = {
Name = "qa-subnet"
}
}
# Security Group
resource "aws_security_group" "qa_sg" {
name = "qa-security-group"
description = "QA environment security group"
vpc_id = aws_vpc.qa_vpc.id
# Allow HTTP from anywhere
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow HTTPS from anywhere
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow SSH from company network only
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["203.0.113.0/24"] # Company IP range
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# EC2 Instance
resource "aws_instance" "qa_app_server" {
ami = "ami-0c55b159cbfafe1f0" # Ubuntu 20.04
instance_type = "t3.medium"
subnet_id = aws_subnet.qa_subnet.id
vpc_security_group_ids = [aws_security_group.qa_sg.id]
tags = {
Name = "qa-app-server"
Environment = "qa"
}
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y docker.io docker-compose
systemctl start docker
systemctl enable docker
EOF
}
# RDS Database
resource "aws_db_instance" "qa_database" {
identifier = "qa-database"
engine = "postgres"
engine_version = "13.7"
instance_class = "db.t3.medium"
allocated_storage = 100
db_name = "myapp_qa"
username = "qa_user"
password = var.db_password # From variables
vpc_security_group_ids = [aws_security_group.qa_sg.id]
db_subnet_group_name = aws_db_subnet_group.qa_subnet_group.name
backup_retention_period = 7
backup_window = "03:00-04:00"
tags = {
Name = "qa-database"
Environment = "qa"
}
}
# ElastiCache Redis
resource "aws_elasticache_cluster" "qa_redis" {
cluster_id = "qa-redis"
engine = "redis"
node_type = "cache.t3.micro"
num_cache_nodes = 1
parameter_group_name = "default.redis6.x"
port = 6379
security_group_ids = [aws_security_group.qa_sg.id]
subnet_group_name = aws_elasticache_subnet_group.qa_cache_subnet.name
tags = {
Name = "qa-redis"
Environment = "qa"
}
}
Step 2: Application Deployment
# Docker Compose for QA Environment
# docker-compose.qa.yml
version: '3.8'
services:
# Application Server
app:
image: myapp:qa-latest
container_name: qa-app
ports:
- "3000:3000"
environment:
- NODE_ENV=qa
- DATABASE_URL=postgresql://qa_user:${DB_PASSWORD}@qa-db:5432/myapp_qa
- REDIS_URL=redis://qa-redis:6379
- API_KEY=${API_KEY}
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
networks:
- qa-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# Database
db:
image: postgres:13
container_name: qa-db
environment:
- POSTGRES_DB=myapp_qa
- POSTGRES_USER=qa_user
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- qa-db-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
networks:
- qa-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U qa_user"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:6-alpine
container_name: qa-redis
ports:
- "6379:6379"
networks:
- qa-network
volumes:
- qa-redis-data:/data
command: redis-server --appendonly yes
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: qa-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/qa.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- qa-network
restart: unless-stopped
volumes:
qa-db-data:
qa-redis-data:
networks:
qa-network:
driver: bridge
Step 3: Test Data Management
# test_data_manager.py
import psycopg2
from faker import Faker
import random
from datetime import datetime, timedelta
class TestDataManager:
"""Manage test data for QA environment"""
def __init__(self, db_connection_string):
self.conn = psycopg2.connect(db_connection_string)
self.fake = Faker()
def reset_database(self):
"""Reset database to clean state"""
with self.conn.cursor() as cursor:
# Clear all tables (respecting foreign keys)
cursor.execute("""
TRUNCATE TABLE
orders,
order_items,
products,
users,
addresses
CASCADE
""")
self.conn.commit()
print("Database reset complete")
def seed_users(self, count=100):
"""Create test users"""
with self.conn.cursor() as cursor:
users = []
for _ in range(count):
user = {
'email': self.fake.email(),
'first_name': self.fake.first_name(),
'last_name': self.fake.last_name(),
'password_hash': '$2b$10$EIXexample', # Pre-hashed test password
'created_at': self.fake.date_time_between(start_date='-1y')
}
users.append(user)
cursor.execute("""
INSERT INTO users (email, first_name, last_name, password_hash, created_at)
VALUES (%(email)s, %(first_name)s, %(last_name)s, %(password_hash)s, %(created_at)s)
""", user)
self.conn.commit()
print(f"Created {count} test users")
def seed_products(self, count=50):
"""Create test products"""
categories = ['Electronics', 'Clothing', 'Books', 'Home', 'Sports']
with self.conn.cursor() as cursor:
for _ in range(count):
product = {
'name': self.fake.catch_phrase(),
'description': self.fake.text(max_nb_chars=200),
'price': round(random.uniform(9.99, 999.99), 2),
'category': random.choice(categories),
'stock': random.randint(0, 1000),
'sku': self.fake.ean13()
}
cursor.execute("""
INSERT INTO products (name, description, price, category, stock, sku)
VALUES (%(name)s, %(description)s, %(price)s, %(category)s, %(stock)s, %(sku)s)
""", product)
self.conn.commit()
print(f"Created {count} test products")
def seed_orders(self, count=200):
"""Create test orders"""
with self.conn.cursor() as cursor:
# Get user IDs
cursor.execute("SELECT id FROM users")
user_ids = [row[0] for row in cursor.fetchall()]
# Get product IDs and prices
cursor.execute("SELECT id, price FROM products")
products = cursor.fetchall()
for _ in range(count):
user_id = random.choice(user_ids)
status = random.choice(['pending', 'processing', 'shipped', 'delivered', 'cancelled'])
created_at = self.fake.date_time_between(start_date='-6m')
# Create order
cursor.execute("""
INSERT INTO orders (user_id, status, created_at, total)
VALUES (%s, %s, %s, 0)
RETURNING id
""", (user_id, status, created_at))
order_id = cursor.fetchone()[0]
# Add 1-5 items to order
order_total = 0
num_items = random.randint(1, 5)
for _ in range(num_items):
product_id, price = random.choice(products)
quantity = random.randint(1, 3)
item_total = price * quantity
order_total += item_total
cursor.execute("""
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (%s, %s, %s, %s)
""", (order_id, product_id, quantity, price))
# Update order total
cursor.execute("""
UPDATE orders SET total = %s WHERE id = %s
""", (order_total, order_id))
self.conn.commit()
print(f"Created {count} test orders")
def create_known_test_accounts(self):
"""Create known test accounts for manual testing"""
test_accounts = [
{
'email': 'test.user@example.com',
'password': 'Test123!',
'first_name': 'Test',
'last_name': 'User',
'role': 'user'
},
{
'email': 'admin@example.com',
'password': 'Admin123!',
'first_name': 'Admin',
'last_name': 'User',
'role': 'admin'
},
{
'email': 'premium@example.com',
'password': 'Premium123!',
'first_name': 'Premium',
'last_name': 'User',
'role': 'premium_user'
}
]
with self.conn.cursor() as cursor:
for account in test_accounts:
cursor.execute("""
INSERT INTO users (email, password_hash, first_name, last_name, role)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (email) DO UPDATE
SET password_hash = EXCLUDED.password_hash
""", (
account['email'],
'$2b$10$EIXexample', # Pre-hashed password
account['first_name'],
account['last_name'],
account['role']
))
self.conn.commit()
print("Created known test accounts")
def refresh_test_data(self):
"""Full refresh of test data"""
print("Starting test data refresh...")
self.reset_database()
self.create_known_test_accounts()
self.seed_users(100)
self.seed_products(50)
self.seed_orders(200)
print("Test data refresh complete!")
# Usage
if __name__ == "__main__":
db_url = "postgresql://qa_user:password@qa-db:5432/myapp_qa"
manager = TestDataManager(db_url)
manager.refresh_test_data()
Step 4: Environment Configuration
# config.py - Environment-specific configuration
import os
from enum import Enum
class Environment(Enum):
DEVELOPMENT = "development"
QA = "qa"
STAGING = "staging"
PRODUCTION = "production"
class Config:
"""Base configuration"""
SECRET_KEY = os.getenv('SECRET_KEY')
DATABASE_URL = os.getenv('DATABASE_URL')
REDIS_URL = os.getenv('REDIS_URL')
# Feature flags
ENABLE_ANALYTICS = False
ENABLE_EMAILS = True
ENABLE_SMS = False
class DevelopmentConfig(Config):
"""Development environment configuration"""
DEBUG = True
LOG_LEVEL = "DEBUG"
# Use local services
EMAIL_BACKEND = "console" # Print to console
PAYMENT_GATEWAY = "mock"
# Relaxed security for dev
ALLOWED_HOSTS = ["*"]
CORS_ORIGINS = ["*"]
class QAConfig(Config):
"""QA environment configuration"""
DEBUG = False
LOG_LEVEL = "INFO"
# Use test services
EMAIL_BACKEND = "mailtrap"
PAYMENT_GATEWAY = "stripe_test"
# QA-specific settings
ALLOWED_HOSTS = ["qa.myapp.com", "qa.internal.company.com"]
CORS_ORIGINS = ["https://qa.myapp.com"]
# Enable feature flags for testing
ENABLE_ANALYTICS = True
ENABLE_EMAILS = True
ENABLE_SMS = True
# Test data refresh
AUTO_REFRESH_TEST_DATA = True
TEST_DATA_REFRESH_TIME = "03:00" # 3 AM daily
class StagingConfig(Config):
"""Staging environment configuration"""
DEBUG = False
LOG_LEVEL = "WARNING"
# Use production-like services
EMAIL_BACKEND = "sendgrid"
PAYMENT_GATEWAY = "stripe_test" # Still test mode
# Production-like settings
ALLOWED_HOSTS = ["staging.myapp.com"]
CORS_ORIGINS = ["https://staging.myapp.com"]
# All features enabled
ENABLE_ANALYTICS = True
ENABLE_EMAILS = True
ENABLE_SMS = True
class ProductionConfig(Config):
"""Production environment configuration"""
DEBUG = False
LOG_LEVEL = "ERROR"
# Production services
EMAIL_BACKEND = "sendgrid"
PAYMENT_GATEWAY = "stripe_live"
# Strict security
ALLOWED_HOSTS = ["myapp.com", "www.myapp.com"]
CORS_ORIGINS = ["https://myapp.com", "https://www.myapp.com"]
# All features enabled
ENABLE_ANALYTICS = True
ENABLE_EMAILS = True
ENABLE_SMS = True
# Environment configuration mapping
config_by_environment = {
Environment.DEVELOPMENT: DevelopmentConfig,
Environment.QA: QAConfig,
Environment.STAGING: StagingConfig,
Environment.PRODUCTION: ProductionConfig
}
def get_config():
"""Get configuration for current environment"""
env = os.getenv('ENVIRONMENT', 'development')
environment = Environment(env)
return config_by_environment[environment]
Containerization with Docker
# Dockerfile for test environment
FROM python:3.9-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
redis-tools \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["python", "app.py"]
Best Practices
1. Environment Isolation
# Environment Isolation Checklist
✓ Separate infrastructure (VPCs, networks)
✓ Separate databases (no shared DB instances)
✓ Separate credentials (unique passwords, API keys)
✓ Separate monitoring (environment-specific alerts)
✓ Network segmentation (firewalls, security groups)
✓ Access control (role-based permissions)
2. Configuration Management
# Use environment variables for configuration
# .env.qa
# Database
DATABASE_HOST=qa-db.internal.company.com
DATABASE_PORT=5432
DATABASE_NAME=myapp_qa
DATABASE_USER=qa_user
DATABASE_PASSWORD=${SECRET_DB_PASSWORD} # From secrets manager
# Redis
REDIS_HOST=qa-redis.internal.company.com
REDIS_PORT=6379
# API Keys (test mode)
STRIPE_API_KEY=sk_test_xxxxx
SENDGRID_API_KEY=SG.test.xxxxx
# Feature Flags
FEATURE_NEW_CHECKOUT=true
FEATURE_AI_RECOMMENDATIONS=false
3. Automated Environment Setup
#!/bin/bash
# setup-qa-environment.sh
set -e
echo "Setting up QA environment..."
# 1. Provision infrastructure
echo "Provisioning infrastructure..."
cd terraform
terraform init
terraform apply -auto-approve
# 2. Deploy application
echo "Deploying application..."
cd ../
docker-compose -f docker-compose.qa.yml up -d
# 3. Wait for services to be healthy
echo "Waiting for services..."
./wait-for-services.sh
# 4. Run database migrations
echo "Running migrations..."
docker-compose -f docker-compose.qa.yml exec app python manage.py migrate
# 5. Load test data
echo "Loading test data..."
docker-compose -f docker-compose.qa.yml exec app python seed_data.py
# 6. Run smoke tests
echo "Running smoke tests..."
pytest tests/smoke/
echo "QA environment setup complete!"
echo "Access at: https://qa.myapp.com"
4. Monitor Environment Health
# environment_health_check.py
import requests
import psycopg2
import redis
from datetime import datetime
class EnvironmentHealthCheck:
"""Monitor test environment health"""
def __init__(self, config):
self.config = config
self.results = []
def check_application(self):
"""Check if application is responding"""
try:
response = requests.get(f"{self.config['app_url']}/health", timeout=5)
if response.status_code == 200:
self.results.append(("Application", "✓ Healthy"))
else:
self.results.append(("Application", f"✗ Unhealthy (Status: {response.status_code})"))
except Exception as e:
self.results.append(("Application", f"✗ Down ({str(e)})"))
def check_database(self):
"""Check database connectivity"""
try:
conn = psycopg2.connect(self.config['database_url'])
with conn.cursor() as cursor:
cursor.execute("SELECT 1")
result = cursor.fetchone()
if result:
self.results.append(("Database", "✓ Healthy"))
conn.close()
except Exception as e:
self.results.append(("Database", f"✗ Down ({str(e)})"))
def check_redis(self):
"""Check Redis connectivity"""
try:
r = redis.from_url(self.config['redis_url'])
r.ping()
self.results.append(("Redis", "✓ Healthy"))
except Exception as e:
self.results.append(("Redis", f"✗ Down ({str(e)})"))
def check_disk_space(self):
"""Check available disk space"""
import shutil
stat = shutil.disk_usage('/')
used_percent = (stat.used / stat.total) * 100
if used_percent < 80:
self.results.append(("Disk Space", f"✓ {used_percent:.1f}% used"))
else:
self.results.append(("Disk Space", f"⚠ {used_percent:.1f}% used (WARNING)"))
def run_all_checks(self):
"""Run all health checks"""
print(f"Environment Health Check - {datetime.now()}")
print("=" * 50)
self.check_application()
self.check_database()
self.check_redis()
self.check_disk_space()
for service, status in self.results:
print(f"{service:.<30} {status}")
print("=" * 50)
# Return True if all checks passed
return all("✓" in status for _, status in self.results)
# Usage
config = {
'app_url': 'https://qa.myapp.com',
'database_url': 'postgresql://qa_user:pass@qa-db:5432/myapp_qa',
'redis_url': 'redis://qa-redis:6379'
}
health_check = EnvironmentHealthCheck(config)
if health_check.run_all_checks():
print("\n✓ Environment is healthy")
exit(0)
else:
print("\n✗ Environment has issues")
exit(1)
Conclusion
A well-configured test environment is foundational to effective testing. By following infrastructure as code practices, maintaining environment parity, managing test data systematically, and automating setup processes, you ensure reliable, repeatable testing that catches issues before they reach production.
Key takeaways:
- Use separate environments for different testing stages
- Automate environment provisioning and configuration
- Manage test data systematically
- Monitor environment health continuously
- Containerize applications for consistency
- Document environment setup procedures
Whether you’re setting up a simple local environment or a complex multi-tier testing infrastructure, these principles and practices will help you build robust, maintainable test environments that support your quality assurance goals.