TerraformPilot

DevOps

Terraform Testing with Terratest: Automated Infrastructure Tests in Go

Test Terraform code with Terratest. Write Go tests that deploy real infrastructure, validate it works, and tear it down. Includes unit tests

LLuca Berton1 min read

What Is Terratest?

#

Terratest is a Go library for testing infrastructure code. It deploys real resources, validates they work, then destroys them:

Write test → terraform apply → validate → terraform destroy

Unlike terraform validate (syntax only) or tfsec (static analysis), Terratest proves your infrastructure actually works by deploying it.

Install Terratest

#
# Initialize Go module
mkdir test && cd test
go mod init github.com/myorg/myproject/test
 
# Add Terratest
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/stretchr/testify/assert

Your First Test

#

Terraform Module

#
# modules/s3-bucket/main.tf
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
 
  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}
 
output "bucket_arn" {
  value = aws_s3_bucket.this.arn
}
 
output "bucket_name" {
  value = aws_s3_bucket.this.id
}

Go Test

#
// test/s3_bucket_test.go
package test
 
import (
    "fmt"
    "strings"
    "testing"
 
    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)
 
func TestS3Bucket(t *testing.T) {
    t.Parallel()
 
    // Random name to avoid collisions
    uniqueID := strings.ToLower(random.UniqueId())
    bucketName := fmt.Sprintf("test-bucket-%s", uniqueID)
    awsRegion := "us-east-1"
 
    // Configure Terraform
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../modules/s3-bucket",
        Vars: map[string]interface{}{
            "bucket_name": bucketName,
            "environment": "test",
        },
        EnvVars: map[string]string{
            "AWS_DEFAULT_REGION": awsRegion,
        },
    })
 
    // Destroy after test
    defer terraform.Destroy(t, terraformOptions)
 
    // Deploy
    terraform.InitAndApply(t, terraformOptions)
 
    // Validate outputs
    outputBucketName := terraform.Output(t, terraformOptions, "bucket_name")
    assert.Equal(t, bucketName, outputBucketName)
 
    // Validate the bucket actually exists in AWS
    aws.AssertS3BucketExists(t, awsRegion, bucketName)
}

Run

#
cd test
go test -v -timeout 30m
=== RUN   TestS3Bucket
    TestS3Bucket: s3_bucket_test.go:30: Running terraform init...
    TestS3Bucket: s3_bucket_test.go:33: Running terraform apply...
    TestS3Bucket: s3_bucket_test.go:38: bucket_name = "test-bucket-abc123"
    TestS3Bucket: s3_bucket_test.go:42: Bucket test-bucket-abc123 exists
    TestS3Bucket: s3_bucket_test.go:45: Running terraform destroy...
--- PASS: TestS3Bucket (120.5s)

Test Patterns

#

EC2 Instance Test

#
func TestEC2Instance(t *testing.T) {
    t.Parallel()
 
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../modules/ec2",
        Vars: map[string]interface{}{
            "instance_type": "t3.micro",
            "environment":   "test",
        },
    })
 
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
 
    // Get the instance ID
    instanceID := terraform.Output(t, terraformOptions, "instance_id")
 
    // Verify instance is running
    instance := aws.GetInstancesByIdE(t, instanceID, "us-east-1")
    assert.Equal(t, "running", instance)
 
    // Verify we can SSH
    publicIP := terraform.Output(t, terraformOptions, "public_ip")
    ssh.CheckSshCommand(t, publicIP, "ubuntu", keyPair, "echo hello")
}

VPC Test

#
func TestVPC(t *testing.T) {
    t.Parallel()
 
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../modules/vpc",
        Vars: map[string]interface{}{
            "vpc_cidr":    "10.99.0.0/16",
            "environment": "test",
        },
    })
 
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
 
    vpcID := terraform.Output(t, terraformOptions, "vpc_id")
    publicSubnets := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    privateSubnets := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
 
    // Validate subnet counts
    assert.Equal(t, 2, len(publicSubnets))
    assert.Equal(t, 2, len(privateSubnets))
 
    // Validate VPC exists
    vpc := aws.GetVpcById(t, vpcID, "us-east-1")
    assert.Equal(t, "10.99.0.0/16", vpc.CidrBlock)
}

Terraform Built-in Tests (v1.6+)

#

Terraform 1.6 added native testing without Go:

# tests/s3_bucket.tftest.hcl
run "create_bucket" {
  command = apply
 
  variables {
    bucket_name = "test-bucket-tftest"
    environment = "test"
  }
 
  assert {
    condition     = output.bucket_name == "test-bucket-tftest"
    error_message = "Bucket name doesn't match"
  }
 
  assert {
    condition     = output.bucket_arn != ""
    error_message = "Bucket ARN should not be empty"
  }
}

Run:

terraform test

When to Use Each

#
Terratest (Go)terraform test (native)
LanguageGoHCL
Deploys real infraYesYes (or plan-only)
HTTP validationYesNo
SSH validationYesNo
Custom logicFull Go stdlibLimited to HCL
CI/CD integrationStandard Go testingBuilt-in
Best forComplex validationQuick assertions

CI/CD Integration

#

GitLab CI

#
test:
  stage: test
  image: golang:1.22
  services:
    - docker:dind
  before_script:
    - apt-get update && apt-get install -y unzip
    - curl -LO https://releases.hashicorp.com/terraform/1.9.0/terraform_1.9.0_linux_amd64.zip
    - unzip terraform_*.zip && mv terraform /usr/local/bin/
  script:
    - cd test
    - go test -v -timeout 30m -count=1
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Best Practices for CI

#
  1. Use t.Parallel() — run tests concurrently
  2. Random names — avoid resource naming conflicts between parallel tests
  3. Set timeoutgo test -timeout 30m (default 10m is too short)
  4. Always defer Destroy — prevent resource leaks
  5. Use WithDefaultRetryableErrors — handles transient AWS errors

Test Organization

#
project/
├── modules/
│   ├── vpc/
│   ├── ec2/
│   └── rds/
├── test/
│   ├── go.mod
│   ├── go.sum
│   ├── vpc_test.go
│   ├── ec2_test.go
│   └── rds_test.go
└── tests/              # Native terraform test
    └── basic.tftest.hcl

Hands-On Courses

#

Learn by doing with interactive courses on CopyPasteLearn:

Conclusion

#

Use Terratest for complex validation (HTTP checks, SSH, custom logic) and terraform test for quick assertions. Both deploy real infrastructure and destroy it after. Always use t.Parallel(), random resource names, and defer Destroy to keep tests fast and clean. Run tests in CI on every merge request to catch infrastructure bugs before they reach production.

#Terraform#Terratest#Testing#Go#DevOps

Share this article