How do you know your Terraform code works? Manual apply in dev environment? That's slow and error-prone. Terratest lets you write Go tests that actually provision infrastructure, validate resources, and clean up automatically. In this lesson, you'll learn to test Terraform modules with confidence.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Install and configure Terratest
  • Write Go tests for Terraform modules
  • Test resource properties and outputs
  • Test AWS infrastructure
  • Integrate tests into CI/CD pipelines
  • Understand test patterns and best practices

2. Why This Matters

Real-world scenario: A junior engineer changes a security group rule. Without testing, they apply directly to staging. Production has the same broken rule. With Terratest, a PR runs tests that verify the security group configuration before merging.

3. Core Concepts

Terratest Setup

# Install Go
# Ubuntu/Debian
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# Verify installation
go version

# Create test directory
mkdir terraform-tests
cd terraform-tests
go mod init terraform-tests

# Install Terratest
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/gruntwork-io/terratest/modules/aws
go get github.com/gruntwork-io/terratest/modules/random

# Directory structure
terraform-tests/
├── go.mod
├── go.sum
├── test/
│   ├── vpc_test.go
│   ├── ec2_test.go
│   └── s3_test.go
└── fixtures/
    ├── vpc/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    ├── ec2/
    │   └── main.tf
    └── s3/
        └── main.tf
Terratest setup

Basic Terratest Example

// test/vpc_test.go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/stretchr/testify/assert"
)

func TestVPC(t *testing.T) {
    t.Parallel()

    // Terraform options
    terraformOptions := &terraform.Options{
        // Path to Terraform code
        TerraformDir: "../fixtures/vpc",

        // Variables to pass
        Vars: map[string]interface{}{
            "vpc_cidr": "10.0.0.0/16",
            "environment": "test",
        },

        // Environment variables
        EnvVars: map[string]string{
            "AWS_DEFAULT_REGION": "us-east-1",
        },
    }

    // Clean up resources after test
    defer terraform.Destroy(t, terraformOptions)

    // Apply Terraform
    terraform.InitAndApply(t, terraformOptions)

    // Get outputs
    vpcID := terraform.Output(t, terraformOptions, "vpc_id")
    
    // Validate VPC exists
    vpc := aws.GetVpcById(t, vpcID, "us-east-1")
    assert.Equal(t, "10.0.0.0/16", vpc.CidrBlock)
    assert.Equal(t, "test", vpc.Tags["Environment"])

    // Get subnet outputs
    publicSubnetIDs := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    assert.Equal(t, 3, len(publicSubnetIDs))

    // Validate each subnet
    for _, subnetID := range publicSubnetIDs {
        subnet := aws.GetSubnetById(t, subnetID, "us-east-1")
        assert.True(t, subnet.MapPublicIpOnLaunch)
    }
}
Basic Terratest for VPC module

Testing EC2 Module

// test/ec2_test.go
package test

import (
    "testing"
    "time"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestEC2Instance(t *testing.T) {
    t.Parallel()

    // Generate unique name for resources
    instanceName := "test-instance-" + random.UniqueId()

    terraformOptions := &terraform.Options{
        TerraformDir: "../fixtures/ec2",
        Vars: map[string]interface{}{
            "instance_name": instanceName,
            "instance_type": "t3.micro",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Get instance ID from output
    instanceID := terraform.Output(t, terraformOptions, "instance_id")
    
    // Validate instance exists
    instance := aws.GetEc2InstanceById(t, instanceID, "us-east-1")
    assert.Equal(t, "t3.micro", instance.InstanceType)
    assert.Equal(t, instanceName, getTagValue(instance.Tags, "Name"))

    // Test HTTP endpoint if web server
    publicIP := terraform.Output(t, terraformOptions, "public_ip")
    if publicIP != "" {
        // Wait for instance to be ready
        time.Sleep(30 * time.Second)
        
        // Test HTTP response
        url := "http://" + publicIP
        // Use HTTP helper to test endpoint
        // assertHTTPResponse(t, url, 200)
    }
}

func getTagValue(tags []*ec2.Tag, key string) string {
    for _, tag := range tags {
        if *tag.Key == key {
            return *tag.Value
        }
    }
    return ""
}
Testing EC2 module

Testing S3 Bucket

// test/s3_test.go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/stretchr/testify/assert"
)

func TestS3Bucket(t *testing.T) {
    t.Parallel()

    bucketName := "test-bucket-" + random.UniqueId()

    terraformOptions := &terraform.Options{
        TerraformDir: "../fixtures/s3",
        Vars: map[string]interface{}{
            "bucket_name": bucketName,
            "environment": "test",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Get bucket region
    region := aws.GetAmazonS3BucketRegion(t, bucketName)
    assert.Equal(t, "us-east-1", region)

    // Check bucket exists
    exists := aws.IsS3BucketExists(t, bucketName)
    assert.True(t, exists)

    // Check bucket versioning
    versioning := aws.GetS3BucketVersioning(t, bucketName, region)
    assert.Equal(t, "Enabled", versioning)

    // Check bucket encryption
    encryption := aws.GetS3BucketEncryption(t, bucketName, region)
    assert.NotNil(t, encryption)

    // Check bucket public access block
    publicAccessBlock := aws.GetS3BucketPublicAccessBlock(t, bucketName, region)
    assert.True(t, publicAccessBlock.BlockPublicAcls)
    assert.True(t, publicAccessBlock.BlockPublicPolicy)

    // Upload test file
    testContent := []byte("Hello, S3!")
    aws.UploadStringToS3(t, region, bucketName, "test.txt", string(testContent))

    // Download and verify
    downloadedContent := aws.DownloadStringFromS3(t, region, bucketName, "test.txt")
    assert.Equal(t, string(testContent), downloadedContent)
}
Testing S3 bucket

Testing with HTTP Validation

// test/http_test.go
package test

import (
    "testing"
    "time"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/http-helper"
    "github.com/stretchr/testify/assert"
)

func TestWebServer(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../fixtures/webserver",
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Get load balancer DNS
    albDNS := terraform.Output(t, terraformOptions, "alb_dns_name")

    // Wait for ALB to be ready
    time.Sleep(60 * time.Second)

    // Test HTTP endpoint
    url := "http://" + albDNS
    statusCode, body := http_helper.HttpGet(t, url, 30, 5)
    
    assert.Equal(t, 200, statusCode)
    assert.Contains(t, body, "Hello from Web Server")

    // Test health endpoint
    healthURL := url + "/health"
    statusCode, body = http_helper.HttpGet(t, healthURL, 30, 5)
    assert.Equal(t, 200, statusCode)
    assert.Contains(t, body, "healthy")

    // Test that we can't access admin endpoint
    adminURL := url + "/admin"
    statusCode, _ = http_helper.HttpGet(t, adminURL, 30, 5)
    assert.Equal(t, 403, statusCode)
}

func TestAPIEndpoint(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../fixtures/api",
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    apiURL := terraform.Output(t, terraformOptions, "api_url")

    // Test GET endpoint
    url := apiURL + "/users"
    statusCode, body := http_helper.HttpGet(t, url, 30, 5)
    assert.Equal(t, 200, statusCode)

    // Test POST endpoint
    postBody := `{"name": "John Doe", "email": "john@example.com"}`
    statusCode, body = http_helper.HttpPost(t, apiURL+"/users", postBody, nil, 30, 5)
    assert.Equal(t, 201, statusCode)

    // Validate response
    assert.Contains(t, body, "John Doe")
}
Testing HTTP endpoints

4. Complete Project: CI/CD Integration

# .github/workflows/terraform-test.yml
name: Terraform Test

on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'test/**'

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.21'
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.6.0
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      
      - name: Cache Go modules
        uses: actions/cache@v3
        with:
          path: |
            ~/.cache/go-mod
            ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-
      
      - name: Run Terratest
        run: |
          cd test
          go mod download
          go test -v -timeout 30m -run TestVPC
          go test -v -timeout 30m -run TestEC2
          go test -v -timeout 30m -run TestS3
        env:
          AWS_DEFAULT_REGION: us-east-1
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: test-results/

  # Parallel test execution
  test-parallel:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        module: [vpc, ec2, s3, rds]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.21'
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      - name: Test Module
        run: |
          cd test
          go test -v -timeout 20m -run Test${{ matrix.module }}

  test-report:
    runs-on: ubuntu-latest
    needs: [test, test-parallel]
    if: always()
    steps:
      - name: Test Summary
        run: |
          echo "## Terraform Test Results" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Module | Status |" >> $GITHUB_STEP_SUMMARY
          echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY
          echo "| VPC | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
          echo "| EC2 | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
          echo "| S3 | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
CI/CD integration for Terratest

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Terraform Lesson 8.6: Terraform in CI/CD

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 21, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment