Terratest Guide
Terratest is a Go library that provides patterns and helper functions for testing infrastructure code. This guide covers how to effectively use Terratest for testing Terraform configurations as of 2025.
Getting Started
Installation
First, ensure you have Go installed:
go version # Should be 1.21 or higher
Create a new test directory and initialize a Go module:
mkdir test
cd test
go mod init terraform-tests
Install Terratest:
go get -u github.com/gruntwork-io/terratest@latest
Basic Test Structure
Directory Layout
infrastructure/
├── modules/
│ └── vpc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── test/
├── go.mod
├── go.sum
└── vpc_test.go
Simple Test Example
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVPCModule(t *testing.T) {
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/vpc",
// Variables to pass to our Terraform code
Vars: map[string]interface{}{
"vpc_name": "test-vpc",
"cidr_block": "10.0.0.0/16",
},
})
// Clean up resources when the test is complete
defer terraform.Destroy(t, terraformOptions)
// Deploy the infrastructure
terraform.InitAndApply(t, terraformOptions)
// Get output variables
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
subnets := terraform.OutputList(t, terraformOptions, "subnet_ids")
// Verify the infrastructure
assert.NotEmpty(t, vpcId)
assert.Equal(t, 3, len(subnets))
}
Advanced Testing Patterns
1. Retry Logic
func TestWithRetry(t *testing.T) {
maxRetries := 3
timeBetweenRetries := 5 * time.Second
retry.DoWithRetry(t, "Deploy infrastructure", maxRetries, timeBetweenRetries,
func() (string, error) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/app",
}
terraform.InitAndApply(t, terraformOptions)
return "", nil
},
)
}
2. HTTP Testing
func TestHTTPEndpoint(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/web-app",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
url := terraform.Output(t, terraformOptions, "app_url")
http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello, World!", 30, 5*time.Second)
}
3. SSH Testing
func TestSSHConnection(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/ec2",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
publicIP := terraform.Output(t, terraformOptions, "public_ip")
keyPair := terraform.Output(t, terraformOptions, "key_pair")
host := ssh.Host{
Hostname: publicIP,
SshKeyPair: keyPair,
Username: "ec2-user",
}
// Run command via SSH
output := ssh.CheckSshCommand(t, host, "echo 'Hello, World!'")
assert.Equal(t, "Hello, World!", output)
}
Testing Cloud-Specific Resources
AWS Resources
func TestAWSResources(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/aws",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Test S3 bucket
bucketName := terraform.Output(t, terraformOptions, "bucket_name")
aws.AssertS3BucketExists(t, "us-west-2", bucketName)
// Test RDS instance
dbAddress := terraform.Output(t, terraformOptions, "db_address")
aws.GetRdsInstanceById(t, dbAddress, "us-west-2")
}
Azure Resources
func TestAzureResources(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/azure",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Test Azure Container Registry
acrName := terraform.Output(t, terraformOptions, "acr_name")
azure.ContainerRegistryExists(t, acrName, "my-resource-group", "my-subscription")
}
Test Fixtures
1. Example Test Fixture
type TestFixture struct {
terraform *terraform.Options
workDir string
}
func setupTestFixture(t *testing.T) *TestFixture {
workDir, err := ioutil.TempDir("", "terraform-test")
require.NoError(t, err)
terraformOptions := &terraform.Options{
TerraformDir: workDir,
Vars: map[string]interface{}{
"environment": "test",
},
}
return &TestFixture{
terraform: terraformOptions,
workDir: workDir,
}
}
func (f *TestFixture) Cleanup() {
os.RemoveAll(f.workDir)
}
2. Using Test Fixtures
func TestWithFixture(t *testing.T) {
fixture := setupTestFixture(t)
defer fixture.Cleanup()
terraform.InitAndApply(t, fixture.terraform)
// Run tests...
}
Test Parallelization
1. Parallel Test Execution
func TestParallelDeployments(t *testing.T) {
t.Parallel()
uniqueId := random.UniqueId()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/app",
Vars: map[string]interface{}{
"app_name": fmt.Sprintf("test-app-%s", uniqueId),
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
}
2. Resource Naming for Parallel Tests
func getUniqueTestName(t *testing.T) string {
return fmt.Sprintf("test-%s-%s", t.Name(), random.UniqueId())
}
func TestMultipleInstances(t *testing.T) {
t.Parallel()
testName := getUniqueTestName(t)
terraformOptions := &terraform.Options{
TerraformDir: "../modules/instance",
Vars: map[string]interface{}{
"instance_name": testName,
},
}
// Deploy and test...
}
Testing Best Practices
1. Environment Cleanup
func ensureCleanup(t *testing.T, options *terraform.Options) {
defer func() {
if err := recover(); err != nil {
terraform.Destroy(t, options)
panic(err)
}
}()
terraform.Destroy(t, options)
}
2. Test Timeouts
func TestWithTimeout(t *testing.T) {
timeout := 30 * time.Minute
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan bool)
go func() {
// Run your test
terraform.InitAndApply(t, terraformOptions)
done <- true
}()
select {
case <-done:
// Test completed successfully
case <-ctx.Done():
t.Fatal("Test timed out")
}
}
3. Test Logging
func TestWithLogging(t *testing.T) {
logger := terraform.NewLogger(t)
defer logger.Cleanup()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/app",
Logger: logger,
}
logger.Logf(t, "Starting test deployment...")
terraform.InitAndApply(t, terraformOptions)
logger.Logf(t, "Deployment complete")
}
CI/CD Integration
name: Terratest
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
- name: Download dependencies
run: |
cd test
go mod download
- name: Run Terratest
run: |
cd test
go test -v -timeout 30m
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Troubleshooting
Common Issues and Solutions
State Lock Issues
func TestWithStateLock(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/app",
Lock: true,
LockTimeout: "5m",
}
terraform.InitAndApply(t, terraformOptions)
}
Provider Authentication
func init() {
os.Setenv("AWS_SDK_LOAD_CONFIG", "true")
os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
}
Resource Dependencies
func TestWithDependencies(t *testing.T) {
// Deploy dependencies first
depOptions := &terraform.Options{
TerraformDir: "../modules/dependencies",
}
terraform.InitAndApply(t, depOptions)
// Get outputs from dependencies
dbEndpoint := terraform.Output(t, depOptions, "db_endpoint")
// Use in main deployment
appOptions := &terraform.Options{
TerraformDir: "../modules/app",
Vars: map[string]interface{}{
"db_endpoint": dbEndpoint,
},
}
terraform.InitAndApply(t, appOptions)
}
Last updated