Infrastructure integration testing is becoming a standard practice: Gruntwork reports that Terratest has over 15,000 GitHub stars and is used by teams at Amazon, Google, and Lyft to validate their infrastructure modules before production deployment. The key insight behind Terratest is that Terraform’s native testing validates state — but state and reality can diverge when provider bugs, cloud API delays, or race conditions cause resources to be created incorrectly while Terraform reports success. Terratest queries actual cloud APIs to verify that your S3 bucket not only “exists” in state but is actually accessible, encrypted, and configured correctly. This Go-based approach requires learning Go but provides the strongest possible guarantee: your infrastructure works in the real cloud, with real API responses, under real conditions. This guide covers the complete Terratest workflow from setup to parallel execution patterns.

TL;DR

  • Terratest tests actual infrastructure via cloud APIs—not Terraform state—catching bugs that native testing misses
  • The defer terraform.Destroy pattern guarantees cleanup even when tests fail, preventing orphaned resources
  • Test stages let you skip slow steps during local development (skip AMI build, reuse deployed infra)

Best for: Teams needing to validate real cloud behavior, not just configuration correctness

Skip if: You only need to validate Terraform syntax/logic (use native terraform test instead)

Read time: 12 minutes

Here’s a hard truth about infrastructure testing: Terraform’s native test framework validates state, not reality. If a provider bug creates a misconfigured resource but reports success, your tests pass while your infrastructure fails.

Terratest solves this by querying actual cloud resources through their native APIs. Your S3 bucket isn’t just “created” in Terraform state—Terratest verifies it actually exists, has the right encryption, and serves content correctly.

Prerequisites

Before starting, ensure you have:

  • Go 1.21+ installed (go version)
  • Terraform 1.0+ installed (terraform version)
  • AWS CLI configured with credentials (aws sts get-caller-identity)
  • Basic Go knowledge (functions, structs, error handling)
  • A dedicated AWS account or sandbox for testing

Step 1: Project Setup

Create a new directory structure for your Terraform module and tests:

mkdir -p terraform-s3-module/test
cd terraform-s3-module

Initialize the Go module:

cd test
go mod init github.com/yourorg/terraform-s3-module/test
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/gruntwork-io/terratest/modules/aws
go get github.com/stretchr/testify/assert

Your go.mod should now reference Terratest v0.53.0 or later.

Step 2: Create a Terraform Module to Test

Create main.tf in the root directory:

variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
}

variable "environment" {
  description = "Environment tag"
  type        = string
  default     = "test"
}

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name

  tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

output "bucket_id" {
  value = aws_s3_bucket.this.id
}

output "bucket_arn" {
  value = aws_s3_bucket.this.arn
}

Step 3: Write Your First Terratest

Create test/s3_bucket_test.go:

package test

import (
	"fmt"
	"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 TestS3BucketCreation(t *testing.T) {
	t.Parallel()

	// Generate unique bucket name to avoid conflicts
	uniqueID := random.UniqueId()
	bucketName := fmt.Sprintf("terratest-example-%s", uniqueID)
	awsRegion := "us-west-2"

	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		TerraformDir: "../",
		Vars: map[string]interface{}{
			"bucket_name": bucketName,
			"environment": "test",
		},
		EnvVars: map[string]string{
			"AWS_DEFAULT_REGION": awsRegion,
		},
	})

	// CRITICAL: Always clean up resources
	defer terraform.Destroy(t, terraformOptions)

	// Deploy infrastructure
	terraform.InitAndApply(t, terraformOptions)

	// Get outputs
	bucketID := terraform.Output(t, terraformOptions, "bucket_id")

	// Validate ACTUAL infrastructure via AWS API
	aws.AssertS3BucketExists(t, awsRegion, bucketID)

	// Verify versioning is actually enabled (not just in state)
	versioning := aws.GetS3BucketVersioning(t, awsRegion, bucketID)
	assert.Equal(t, "Enabled", versioning)

	// Verify encryption configuration
	encryption := aws.GetS3BucketEncryption(t, awsRegion, bucketID)
	assert.Equal(t, "AES256", encryption)
}

Key patterns in this test:

  1. t.Parallel() — Runs tests concurrently for faster execution
  2. random.UniqueId() — Prevents resource name collisions
  3. defer terraform.Destroy() — Guarantees cleanup even on failure
  4. aws.AssertS3BucketExists() — Validates real AWS resources, not state

Step 4: Run the Test

Execute from the test directory:

go test -v -timeout 30m

Expected output:

=== RUN   TestS3BucketCreation
=== PAUSE TestS3BucketCreation
=== CONT  TestS3BucketCreation
TestS3BucketCreation 2026-01-11T10:15:30Z command.go:158: Running command terraform with args [init]
TestS3BucketCreation 2026-01-11T10:15:35Z command.go:158: Running command terraform with args [apply -auto-approve]
...
TestS3BucketCreation 2026-01-11T10:16:45Z command.go:158: Running command terraform with args [destroy -auto-approve]
--- PASS: TestS3BucketCreation (95.23s)
PASS

Step 5: Test Stages for Faster Iteration

Real-world tests are slow. Building AMIs, deploying clusters, and running validations can take 30+ minutes. Test stages let you skip completed stages during development:

package test

import (
	"testing"

	"github.com/gruntwork-io/terratest/modules/terraform"
	test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
)

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

	workingDir := test_structure.CopyTerraformFolderToTemp(t, "../", ".")

	// Stage: Deploy
	defer test_structure.RunTestStage(t, "teardown", func() {
		terraformOptions := test_structure.LoadTerraformOptions(t, workingDir)
		terraform.Destroy(t, terraformOptions)
	})

	test_structure.RunTestStage(t, "deploy", func() {
		uniqueID := random.UniqueId()
		terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
			TerraformDir: workingDir,
			Vars: map[string]interface{}{
				"bucket_name": fmt.Sprintf("terratest-%s", uniqueID),
			},
		})
		test_structure.SaveTerraformOptions(t, workingDir, terraformOptions)
		terraform.InitAndApply(t, terraformOptions)
	})

	// Stage: Validate
	test_structure.RunTestStage(t, "validate", func() {
		terraformOptions := test_structure.LoadTerraformOptions(t, workingDir)
		bucketID := terraform.Output(t, terraformOptions, "bucket_id")
		aws.AssertS3BucketExists(t, "us-west-2", bucketID)
	})
}

Skip stages during development:

# Skip deploy, only run validation (assumes infra exists)
SKIP_deploy=true go test -v -run TestWithStages

# Skip teardown to keep resources for debugging
SKIP_teardown=true go test -v -run TestWithStages

AI-Assisted Approaches

In 2026, AI accelerates Terratest development significantly.

What AI does well:

  • Generating boilerplate test structure from Terraform modules
  • Writing assertions based on resource configurations
  • Suggesting edge cases (what if bucket name has special characters?)
  • Converting HCL outputs to Go struct mappings

What still needs humans:

  • Deciding what behaviors actually matter to test
  • Understanding blast radius if tests fail in production accounts
  • Architecting test isolation strategies
  • Reviewing AI-generated assertions for false positives

Useful prompt:

Given this Terraform module that creates an RDS instance:

resource "aws_db_instance" "main" {
  identifier           = var.db_identifier
  engine               = "postgres"
  engine_version       = "15.4"
  instance_class       = var.instance_class
  allocated_storage    = 20
  storage_encrypted    = true
  deletion_protection  = var.enable_deletion_protection
}

Generate a Terratest that:

1. Deploys the RDS instance with test variables
2. Validates encryption is actually enabled via AWS API
3. Verifies the correct Postgres version is running
4. Tests that deletion protection is set correctly
5. Uses proper cleanup with defer

Include error handling and meaningful assertion messages.

Decision Framework

When to Use Terratest

This approach works best when:

  • You need to validate actual cloud resource behavior
  • Testing interactions between multiple services (VPC + EC2 + RDS)
  • Your team has Go experience or willingness to learn
  • Integration tests are critical to your deployment pipeline
  • You’re testing Kubernetes, Docker, or Packer alongside Terraform

Consider alternatives when:

  • You only need to validate Terraform logic (use terraform test)
  • Fast feedback is critical and Go learning curve is too steep
  • Your infrastructure is simple (<5 resources)
  • Cost constraints limit ephemeral resource deployment

Terratest vs Terraform Native Test

AspectTerratestTerraform Test
What it testsActual infrastructure via APIsTerraform state
LanguageGoHCL
Learning curveHigherLower
Catches provider bugsYesNo
Multi-tool supportTerraform, K8s, Docker, PackerTerraform/OpenTofu only
SpeedSlower (real deploys)Faster (plan-based)

My recommendation: Use both. Terraform native tests for fast unit testing of module logic. Terratest for integration tests that validate real infrastructure before production deployments.

Measuring Success

MetricBaselineTargetHow to Track
Test coverage0%80%+ of critical modulesCount tested vs total modules
Test runtimeN/A< 30 min for full suiteCI pipeline metrics
Orphaned resourcesUnknown0AWS Cost Explorer tags
Flaky test rateHigh< 5%CI failure analysis
Mean time to detect issuesDaysHoursIncident tracking

Warning signs:

  • Tests pass but production deployments fail — tests aren’t comprehensive enough
  • Orphaned resources accumulating — cleanup not working properly
  • Tests take > 1 hour — need to parallelize or use test stages
  • High flakiness — missing retry logic or timing issues

Common Pitfalls

1. Missing defer Destroy

// BAD: If Apply fails, resources are orphaned
terraform.InitAndApply(t, options)
terraform.Destroy(t, options) // Never reached if Apply fails

// GOOD: Cleanup runs regardless of test outcome
defer terraform.Destroy(t, options)
terraform.InitAndApply(t, options)

2. Resource Name Collisions

// BAD: Will conflict with other tests or runs
bucketName := "my-test-bucket"

// GOOD: Unique per test run
uniqueID := random.UniqueId()
bucketName := fmt.Sprintf("test-%s", uniqueID)

3. Hardcoded Regions

// BAD: Fails if default region differs
aws.AssertS3BucketExists(t, "us-east-1", bucketID)

// GOOD: Consistent with Terraform deployment
awsRegion := terraform.Output(t, options, "region")
aws.AssertS3BucketExists(t, awsRegion, bucketID)

4. Insufficient Timeouts

# BAD: Default 10m timeout kills long-running tests
go test -v

# GOOD: Allow time for real infrastructure
go test -v -timeout 30m

“Terraform state says your infrastructure exists. Terratest proves it works. That difference matters when the alternative is discovering a misconfigured VPC in a production incident at 2 AM.” — Yuri Kan, Senior QA Lead

What’s Next

Start with one critical module—your VPC, primary database, or main compute cluster. Write a single test that validates it actually works, not just deploys. Run it in CI before every merge.

Once comfortable, expand to test interactions: can your application actually connect to that database? Does the load balancer route traffic correctly?

The goal isn’t 100% coverage—it’s confidence that your infrastructure works in reality, not just in Terraform’s view of reality.

Related articles:

External resources:

FAQ

What is Terratest and how does it work?

Terratest is a Go library from Gruntwork that enables integration testing for infrastructure code. It deploys real infrastructure via Terraform, queries actual cloud resources through their APIs, runs assertions, and destroys everything with defer terraform.Destroy. Unlike Terraform’s native test framework, Terratest validates actual cloud behavior, not just state. Gruntwork reports 15,000+ GitHub stars and adoption by teams at Amazon, Google, and Lyft.

How much does running Terratest cost in AWS?

Most Terratest test suites for a single module run 5-15 minutes and cost $0.10-$2.00 per run depending on resources. Use test stages to skip re-deployment during local development, run tests in parallel to reduce total time, and use lower-cost instance types for testing. Dedicated test AWS accounts with cost alerts prevent surprises.

Can Terratest be used with Terraform Cloud or other IaC tools?

Yes. Terratest supports Terraform, Terragrunt, Packer, Docker, Kubernetes, and other tools. It includes helpers for AWS, GCP, Azure, and Kubernetes API verification. The core pattern (deploy, verify, destroy) works regardless of the IaC tool.

How do you prevent orphaned resources from Terratest runs?

Use defer terraform.Destroy(t, terraformOptions) immediately after configuring options — Go’s defer guarantees execution even on test failure. Run Terratest in dedicated test accounts with automated cleanup scripts (cloud-nuke from Gruntwork) as a safety net for tests that panic unexpectedly.

Official Resources

See Also