Organize tests into maintainable structures with helper modules, clear patterns, and quality standards...
Structure tests for maintainability, clarity, and comprehensive coverage. This skill covers test organization applicable across testing frameworks and programming languages.
Standard Layout:
module-root/
āāā main.tf
āāā variables.tf
āāā outputs.tf
āāā tests/
āāā basic.tftest.hcl # Simple test
āāā integration.tftest.hcl # Integration tests
āāā unit.tftest.hcl # Unit tests (mocked)
āāā setup/ # Setup helper module
ā āāā main.tf
ā āāā variables.tf
ā āāā outputs.tf
āāā final/ # Validation helper module
ā āāā main.tf
āāā mocks/ # Shared mock data
āāā aws.tfmock.hcl
Standard Layout:
src/
āāā services/
ā āāā userService.js
ā āāā __tests__/
ā āāā userService.test.js
ā āāā userService.integration.test.js
āāā utils/
ā āāā validation.js
ā āāā __tests__/
ā āāā validation.test.js
āāā __tests__/
āāā setup.js # Test setup
āāā fixtures/ # Test data
ā āāā users.js
ā āāā products.js
āāā helpers/ # Test utilities
āāā database.js
āāā api.js
Alternative (tests/ directory):
src/
āāā services/
āāā userService.js
tests/
āāā unit/
ā āāā services/
ā āāā userService.test.js
āāā integration/
ā āāā services/
ā āāā userService.integration.test.js
āāā fixtures/
ā āāā users.js
āāā helpers/
āāā database.js
Standard Layout:
src/
āāā myapp/
āāā services/
ā āāā user_service.py
āāā utils/
āāā validation.py
tests/
āāā unit/
ā āāā test_user_service.py
ā āāā test_validation.py
āāā integration/
ā āāā test_user_service_integration.py
āāā fixtures/
ā āāā __init__.py
ā āāā users.py
āāā helpers/
ā āāā __init__.py
ā āāā database.py
āāā conftest.py # Shared fixtures
Standard Layout:
pkg/
āāā services/
āāā user.go
āāā user_test.go # Tests next to code
internal/
āāā database/
āāā db.go
āāā db_test.go
test/
āāā integration/ # Integration tests
ā āāā user_integration_test.go
āāā fixtures/ # Test data
ā āāā users.go
āāā helpers/ # Test utilities
āāā database.go
Setup Module (tests/setup/):
# tests/setup/main.tf
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "3.5.1"
}
}
}
resource "random_pet" "prefix" {
length = 4
}
output "prefix" {
value = random_pet.prefix.id
}
Using Setup Module:
# tests/integration.tftest.hcl
run "setup" {
module {
source = "./tests/setup"
}
}
run "create_resources" {
variables {
name = "${run.setup.prefix}-resource"
}
}
Validation Module (tests/final/):
# tests/final/main.tf
terraform {
required_providers {
http = {
source = "hashicorp/http"
version = "3.4.0"
}
}
}
variable "endpoint" {
type = string
}
data "http" "health" {
url = var.endpoint
}
output "status_code" {
value = data.http.health.status_code
}
Database Helper:
// tests/helpers/database.js
export class TestDatabase {
constructor() {
this.data = new Map();
}
async insert(table, record) {
const key = `${table}:${record.id}`;
this.data.set(key, record);
return record;
}
async findById(table, id) {
return this.data.get(`${table}:${id}`);
}
clear() {
this.data.clear();
}
}
API Helper:
// tests/helpers/api.js
import request from 'supertest';
export class TestApiClient {
constructor(app) {
this.app = app;
}
async post(path, data) {
const response = await request(this.app)
.post(path)
.send(data)
.expect(200);
return response.body;
}
async get(path) {
const response = await request(this.app)
.get(path)
.expect(200);
return response.body;
}
}
conftest.py (shared fixtures):
# tests/conftest.py
import pytest
from myapp.database import Database
@pytest.fixture
def database():
"""Provide test database instance."""
db = Database(':memory:')
db.setup()
yield db
db.teardown()
@pytest.fixture
def mock_users():
"""Provide sample user data."""
return [
{'id': 1, 'name': 'John', 'email': 'john@example.com'},
{'id': 2, 'name': 'Jane', 'email': 'jane@example.com'}
]
@pytest.fixture
def api_client(database):
"""Provide configured API client."""
from myapp.app import create_app
app = create_app(database)
return app.test_client()
Using Fixtures:
# tests/unit/test_user_service.py
def test_create_user(database, mock_users):
service = UserService(database)
user = service.create_user(mock_users[0])
assert user['id'] == 1
Database Helper:
// test/helpers/database.go
package helpers
type TestDatabase struct {
data map[string]interface{}
}
func NewTestDatabase() *TestDatabase {
return &TestDatabase{
data: make(map[string]interface{}),
}
}
func (db *TestDatabase) Insert(key string, value interface{}) error {
db.data[key] = value
return nil
}
func (db *TestDatabase) Get(key string) (interface{}, error) {
val, exists := db.data[key]
if !exists {
return nil, errors.New("not found")
}
return val, nil
}
Using Helper:
// pkg/services/user_test.go
func TestCreateUser(t *testing.T) {
db := helpers.NewTestDatabase()
service := NewUserService(db)
user, err := service.CreateUser("John")
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
Terraform:
# Setup dependencies
run "setup" {
module {
source = "./tests/setup"
}
}
# Execute test
run "create_infrastructure" {
variables {
dependency_id = run.setup.output_id
}
}
# Validate results
run "validate" {
module {
source = "./tests/final"
}
variables {
target = run.create_infrastructure.endpoint
}
assert {
condition = output.status_code == 200
error_message = "Validation failed"
}
}
JavaScript:
describe('User Service', () => {
let database;
let service;
// Setup
beforeEach(() => {
database = new TestDatabase();
service = new UserService(database);
});
// Execute + Validate
it('creates user', async () => {
const user = await service.createUser({ name: 'John' });
expect(user.name).toBe('John');
});
// Cleanup
afterEach(() => {
database.clear();
});
});
Python:
class TestUserService:
# Setup
def setup_method(self):
self.database = TestDatabase()
self.service = UserService(self.database)
# Execute + Validate
def test_create_user(self):
user = self.service.create_user({'name': 'John'})
assert user['name'] == 'John'
# Cleanup
def teardown_method(self):
self.database.clear()
Terraform:
test {
parallel = true
}
run "test_scenario_a" {
state_key = "scenario_a"
# Independent state
}
run "test_scenario_b" {
state_key = "scenario_b"
# Independent state
}
run "test_scenario_c" {
state_key = "scenario_c"
# Independent state
}
JavaScript:
// Tests run in parallel by default in Jest
describe('User Service', () => {
it('scenario A', () => { /* independent test */ });
it('scenario B', () => { /* independent test */ });
it('scenario C', () => { /* independent test */ });
});
Terraform:
# Phase 1: Plan validation
run "plan_validation" {
command = plan
assert {
condition = aws_s3_bucket.bucket.bucket == "expected"
error_message = "Plan-time validation failed"
}
}
# Phase 2: Apply validation
run "apply_validation" {
command = apply
assert {
condition = can(regex("^arn:aws:", aws_s3_bucket.bucket.arn))
error_message = "Resource not created"
}
}
# Phase 3: Runtime validation
run "runtime_validation" {
module {
source = "./tests/final"
}
assert {
condition = output.status_code == 200
error_message = "Runtime validation failed"
}
}
Terraform:
# Test multiple configurations
run "test_dev" {
variables {
environment = "dev"
size = "small"
}
}
run "test_staging" {
variables {
environment = "staging"
size = "medium"
}
}
run "test_prod" {
variables {
environment = "prod"
size = "large"
}
}
JavaScript (Parameterized):
describe.each([
{ env: 'dev', size: 'small' },
{ env: 'staging', size: 'medium' },
{ env: 'prod', size: 'large' }
])('Environment: $env', ({ env, size }) => {
it(`configures ${env} correctly`, () => {
const config = createConfig(env, size);
expect(config.environment).toBe(env);
expect(config.size).toBe(size);
});
});
Python (Parametrize):
import pytest
@pytest.mark.parametrize("env,size", [
("dev", "small"),
("staging", "medium"),
("prod", "large")
])
def test_environment_config(env, size):
config = create_config(env, size)
assert config['environment'] == env
assert config['size'] == size
Descriptive Test Names:
ā Good:
test('creates user with valid email')
test('throws error when email is invalid')
test('updates user profile successfully')
ā Bad:
test('test1')
test('user test')
test('works')
Pattern: <action>_<scenario>_<expected_result>
Terraform:
run "validate_bucket_name_format"
run "test_encryption_enabled"
run "verify_tags_applied"
Python:
def test_create_user_with_valid_email()
def test_create_user_raises_error_when_email_invalid()
def test_update_user_profile_successfully()
ā Good - Independent tests:
test('user creation', () => {
const db = new TestDatabase();
const service = new UserService(db);
const user = service.createUser({ name: 'John' });
expect(user.name).toBe('John');
});
test('user update', () => {
const db = new TestDatabase();
const service = new UserService(db);
service.createUser({ id: 1, name: 'John' });
service.updateUser(1, { name: 'Jane' });
const user = service.getUser(1);
expect(user.name).toBe('Jane');
});
ā Bad - Tests depend on order:
let globalDb;
let globalService;
test('create user', () => {
globalDb = new TestDatabase();
globalService = new UserService(globalDb);
globalService.createUser({ id: 1, name: 'John' });
});
test('update user', () => {
// Depends on previous test!
globalService.updateUser(1, { name: 'Jane' });
});
Use beforeEach/afterEach:
JavaScript:
describe('User Service', () => {
let database;
let service;
beforeEach(() => {
database = new TestDatabase();
service = new UserService(database);
});
afterEach(() => {
database.clear();
});
test('creates user', () => {
// Fresh database for each test
});
});
Python:
class TestUserService:
def setup_method(self):
self.database = TestDatabase()
self.service = UserService(self.database)
def teardown_method(self):
self.database.clear()
def test_creates_user(self):
# Fresh database for each test
pass
Aim for:
Test Coverage Types:
Example:
// Happy path
test('creates user with valid data', () => {
const user = service.createUser({ name: 'John', email: 'john@example.com' });
expect(user).toBeDefined();
});
// Edge case - empty name
test('handles empty name', () => {
const user = service.createUser({ name: '', email: 'john@example.com' });
expect(user.name).toBe('');
});
// Error case - invalid email
test('throws error for invalid email', () => {
expect(() => {
service.createUser({ name: 'John', email: 'invalid' });
}).toThrow('Invalid email');
});
// Integration
test('persists user to database', async () => {
const user = await service.createUser({ name: 'John' });
const retrieved = await database.findById('users', user.id);
expect(retrieved).toEqual(user);
});
Before finalizing test suite:
*.test.js, test_*.py, *_test.go, *.tftest.hcl)Group Related Tests:
describe('UserService', () => {
describe('createUser', () => {
it('creates user with valid data', () => {});
it('throws error for invalid email', () => {});
});
describe('updateUser', () => {
it('updates existing user', () => {});
it('throws error for non-existent user', () => {});
});
});
Setup/Teardown:
beforeAll(() => {
// Runs once before all tests
});
afterAll(() => {
// Runs once after all tests
});
beforeEach(() => {
// Runs before each test
});
afterEach(() => {
// Runs after each test
});
Fixtures:
@pytest.fixture(scope="module")
def database():
"""Module-scoped fixture - shared across module"""
db = Database()
yield db
db.close()
@pytest.fixture(scope="function")
def user():
"""Function-scoped fixture - new for each test"""
return {'id': 1, 'name': 'John'}
Markers:
@pytest.mark.slow
def test_slow_operation():
pass
@pytest.mark.integration
def test_database_integration():
pass
# Run with: pytest -m "not slow"
Subtests:
func TestUserService(t *testing.T) {
t.Run("CreateUser", func(t *testing.T) {
t.Run("with valid data", func(t *testing.T) {
// Test implementation
})
t.Run("with invalid email", func(t *testing.T) {
// Test implementation
})
})
}
Table-Driven Tests:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"valid email", "john@example.com", false},
{"invalid email", "invalid", true},
{"empty email", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
ā Organize tests in logical directories ā Use descriptive test names ā Keep tests independent and isolated ā Create reusable helper modules/fixtures ā Separate unit and integration tests ā Use setup/teardown appropriately ā Test happy path, edge cases, and errors ā Aim for good coverage (80%+) ā Keep tests focused (one concern per test) ā Make tests run quickly
ā Don't create test dependencies (order matters) ā Don't use global state between tests ā Don't skip cleanup/teardown ā Don't test implementation details ā Don't create overly complex test helpers ā Don't duplicate test logic ā Don't ignore slow tests ā Don't mix unit and integration tests