Testing
Infrastructure testing pyramid - static analysis, Terraform test framework, Terratest integration tests, policy-as-code, and CI pipeline integration
Testing
Infrastructure code changes production systems. A bug in a Terraform module can delete a database or open a security group to the internet. Testing catches these mistakes before they reach terraform apply.
The Infrastructure Testing Pyramid
Like application code, infrastructure tests form a pyramid -- fast, cheap tests at the bottom and slow, expensive tests at the top:
┌───────────┐
│ E2E │ Deploy full environment, run smoke tests
─┤ ├─
┌─┴───────────┴─┐
│ Integration │ Apply real resources, validate behavior
─┤ ├─
┌─┴────────────────┴─┐
│ Unit / Contract │ terraform test, mock providers
─┤ ├─
┌─┴────────────────────┴─┐
│ Static Analysis │ validate, lint, scan, fmt
└─────────────────────────┘| Layer | Speed | Cost | Catches |
|---|---|---|---|
| Static analysis | Seconds | Free | Syntax, style, known misconfigurations, security issues |
| Unit tests | Seconds | Free | Logic errors in modules, variable validation |
| Integration tests | Minutes | Cloud costs | Real resource behavior, API compatibility |
| E2E tests | 10-30 min | Cloud costs | Full stack interactions, cross-module issues |
Static Analysis
terraform validate and fmt
# Check syntax and internal consistency
terraform init -backend=false
terraform validate
# Format check (CI should fail on unformatted code)
terraform fmt -check -recursiveTFLint
Catches provider-specific mistakes that validate misses:
# Install and initialize
tflint --init
# Run against a module
tflint --recursiveConfigure .tflint.hcl with provider-specific plugins (e.g., tflint-ruleset-aws) and enable rules like terraform_naming_convention and terraform_documented_variables.
Security Scanning
Multiple tools catch security misconfigurations before deploy:
tfsec . # Terraform-specific security scanner
checkov -d . --framework terraform # policy-as-code for IaC
trivy config . # unified scanner (containers, SBOM, IaC)Unit Testing with terraform test
The native terraform test framework (Terraform 1.6+) runs tests without deploying real infrastructure.
Test File Structure
modules/vpc/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── defaults.tftest.hcl
└── custom_cidr.tftest.hclBasic Test
# tests/defaults.tftest.hcl
# Override the provider to avoid real API calls
mock_provider "aws" {}
variables {
name = "test-vpc"
cidr_block = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
}
run "creates_vpc_with_correct_cidr" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block does not match input"
}
assert {
condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "DNS hostnames should be enabled by default"
}
}
run "creates_correct_number_of_subnets" {
command = plan
assert {
condition = length(aws_subnet.private) == 2
error_message = "Expected 2 private subnets, one per AZ"
}
}Test variable validation by passing invalid inputs and using expect_failures to assert the correct variable raises an error.
Running Tests
# Run all tests in the module
terraform test
# Run a specific test file
terraform test -filter=tests/defaults.tftest.hcl
# Verbose output
terraform test -verboseIntegration Testing with Terratest
Terratest deploys real infrastructure, runs assertions, then destroys everything. Written in Go.
Basic Terratest Example
// test/vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVpcModule(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"name": "test-vpc",
"cidr_block": "10.99.0.0/16",
"azs": []string{"us-east-1a", "us-east-1b"},
},
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": "us-east-1",
},
}
// Destroy resources after test
defer terraform.Destroy(t, terraformOptions)
// Deploy the module
terraform.InitAndApply(t, terraformOptions)
// Validate outputs
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
// Validate the VPC exists in AWS
vpc := aws.GetVpcById(t, vpcId, "us-east-1")
assert.Equal(t, "10.99.0.0/16", vpc.CidrBlock)
// Validate subnets
subnets := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
assert.Len(t, subnets, 2)
}For expensive tests, use test_structure.RunTestStage to split into deploy/validate/teardown stages. Set SKIP_teardown=true during development to keep resources alive for debugging.
Policy Testing
OPA / Conftest
Write policies in Rego and test them against terraform plan output:
# Generate plan JSON
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
# Test against policies
conftest test tfplan.json -p policy/# policy/security.rego
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
resource.change.after.from_port <= 22
resource.change.after.to_port >= 22
msg := sprintf("Security group rule '%s' allows SSH from 0.0.0.0/0", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.server_side_encryption_configuration
msg := sprintf("S3 bucket '%s' does not have encryption enabled", [resource.address])
}For Terraform Cloud / Enterprise, HashiCorp Sentinel provides native policy enforcement with its own policy language.
CI Pipeline Integration
A typical GitHub Actions workflow has two jobs:
- Validate -- runs
fmt -check,validate,tflint,trivy config, andterraform teston all modules - Plan -- runs
terraform plan, posts the output as a PR comment for review
# .github/workflows/terraform.yml
name: Terraform CI
on:
pull_request:
paths: ["infrastructure/**"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive infrastructure/
- run: tflint --init && tflint --recursive infrastructure/
- run: trivy config infrastructure/
plan:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: terraform -chdir=infrastructure/production/us-east-1/vpc init
- run: terraform -chdir=infrastructure/production/us-east-1/vpc plan -no-colorCost Estimation
Use Infracost to check cost impact before applying:
infracost breakdown --path infrastructure/production/us-east-1/vpc
infracost diff --path infrastructure/production/us-east-1/vpc --compare-to infracost-base.jsonDrift Detection and Pre-Publish Testing
Detect when real infrastructure diverges from state by running terraform plan -detailed-exitcode on a nightly CI cron. Exit code 2 means changes detected -- alert the team.
Before publishing a module to a registry, run the full suite: fmt -check, validate, tflint, trivy, terraform test, and go test for integration tests. Then tag and push.
What's Next
Now see where these runs can live: managed platforms that host state, runs, and policy → HCP Terraform & Automation Platforms.