Unit Testing
Unit testing in Terraform involves testing individual modules or resources in isolation to ensure they work as expected. This guide covers modern unit testing approaches for Terraform as of 2025.
Test Framework Options
1. Terratest
Terratest is the most popular testing framework for Terraform. Here's a basic example:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestTerraformModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/complete",
Vars: map[string]interface{}{
"environment": "test",
"region": "us-west-2",
},
}
// Clean up resources after the test
defer terraform.Destroy(t, terraformOptions)
// Deploy the infrastructure
terraform.InitAndApply(t, terraformOptions)
// Validate the outputs
output := terraform.Output(t, terraformOptions, "instance_id")
assert.NotEmpty(t, output)
}
2. Built-in Testing Framework
As of Terraform 1.6+, there's a built-in testing framework:
variables {
environment = "test"
region = "us-west-2"
}
run "verify_vpc_creation" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block must be 10.0.0.0/16"
}
}
run "verify_subnet_creation" {
command = plan
assert {
condition = length(aws_subnet.private) == 3
error_message = "Must create 3 private subnets"
}
}
Best Practices
1. Test Structure
Organize your tests following this structure:
module/
├── main.tf
├── variables.tf
├── outputs.tf
└── test/
├── main_test.go
├── fixtures/
│ ├── complete/
│ │ └── main.tf
│ └── minimal/
│ └── main.tf
└── helper/
└── helper.go
2. Test Cases to Include
Input validation
Resource creation
Output verification
Error handling
Edge cases
Security configurations
3. Mocking Strategies
// Mock AWS provider responses
provider "aws" {
region = "us-west-2"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
ec2 = "http://localhost:4566"
iam = "http://localhost:4566"
}
}
4. Automated Validation
Set up pre-commit hooks for automated testing:
repos:
- repo: local
hooks:
- id: terraform-test
name: Terraform Unit Tests
entry: make test
language: system
files: \.tf$
pass_filenames: false
Integration with CI/CD
GitHub Actions Example
name: Terraform Tests
on:
pull_request:
paths:
- '**.tf'
- '**.go'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Run Tests
run: |
cd test
go test -v ./...
Common Testing Patterns
1. Resource Configuration Testing
func TestResourceTags(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/tags",
Vars: map[string]interface{}{
"tags": map[string]string{
"Environment": "test",
"Project": "example",
},
},
}
output := terraform.PlanAndShowWithStruct(t, terraformOptions)
resourceChanges := output.ResourceChangesMap
// Verify all resources have required tags
for _, resource := range resourceChanges {
tags := resource.Change.After.(map[string]interface{})["tags"]
assert.Contains(t, tags, "Environment")
assert.Contains(t, tags, "Project")
}
}
2. Security Configuration Testing
func TestSecurityGroupRules(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/security",
}
plan := terraform.InitAndPlan(t, terraformOptions)
// Verify no security groups allow 0.0.0.0/0 ingress
terraform.RequirePlannedValuesMapKeyExists(t, plan, "aws_security_group.example")
securityGroup := plan.ResourcePlannedValuesMap["aws_security_group.example"]
ingressRules := securityGroup.AttributeValues["ingress"].([]interface{})
for _, rule := range ingressRules {
cidrBlocks := rule.(map[string]interface{})["cidr_blocks"].([]string)
assert.NotContains(t, cidrBlocks, "0.0.0.0/0")
}
}
Troubleshooting
Common Issues and Solutions
Test Cleanup Failures
// Use custom cleanup logic defer func() { terraform.Destroy(t, terraformOptions) cleanupArtifacts(t, terraformOptions) }()
Parallel Test Conflicts
func TestParallel(t *testing.T) { t.Parallel() uniqueID := random.UniqueId() terraformOptions := &terraform.Options{ TerraformDir: "../examples/complete", Vars: map[string]interface{}{ "name_prefix": fmt.Sprintf("test-%s", uniqueID), }, } // ... rest of test }
Provider Authentication
os.Setenv("AWS_DEFAULT_REGION", "us-west-2") os.Setenv("AWS_SDK_LOAD_CONFIG", "true")
Last updated