Skip to main content
Terraform Testing with Terratest: Automated Infrastructure Tests in Go

Terraform Testing with Terratest: Automated Infrastructure Tests in Go

Key Takeaway

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

Table of Contents

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.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.