You write a Terraform module. You run terraform plan. It looks right. But does it actually create working infrastructure? Terratest deploys your Terraform, validates the result, and tears it down ā automated infrastructure integration tests.
How Terratest Works
Test starts ā terraform init/apply ā Validate resources ā terraform destroy ā Test endsTerratest calls Terraform, waits for resources to be created, runs assertions against them, and cleans up.
Basic Test
// test/vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVpc(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"vpc_cidr": "10.0.0.0/16",
"environment": "test",
"subnet_count": 2,
},
}
// Clean up after test
defer terraform.Destroy(t, terraformOptions)
// Deploy
terraform.InitAndApply(t, terraformOptions)
// Validate outputs
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
subnetIds := terraform.OutputList(t, terraformOptions, "subnet_ids")
assert.Equal(t, 2, len(subnetIds))
}cd test/
go test -v -timeout 30mMaster this topic with hands-on labs
Go beyond reading ā build real projects in sandboxed environments with expert video guidance.
Browse Courses āHTTP Validation
Test that deployed infrastructure actually works:
func TestWebServer(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/web-server",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get the public URL
url := terraform.Output(t, terraformOptions, "url")
// Validate HTTP response
http_helper.HttpGetWithRetry(t, url, nil, 200, "Hello, World!", 30, 10*time.Second)
}Terratest retries the HTTP request up to 30 times (infrastructure might take a moment to be ready).
SSH Validation
func TestSshAccess(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/bastion",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
publicIp := terraform.Output(t, terraformOptions, "public_ip")
keyPair := terraform.Output(t, terraformOptions, "private_key")
host := ssh.Host{
Hostname: publicIp,
SshUserName: "ubuntu",
SshKeyPair: &ssh.KeyPair{PrivateKey: keyPair},
}
// Run command over SSH
output := ssh.CheckSshCommand(t, host, "echo 'Hello from $(hostname)'")
assert.Contains(t, output, "Hello from")
}Testing Kubernetes Resources
func TestKubernetesDeployment(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/k8s-app",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
kubeconfig := terraform.Output(t, terraformOptions, "kubeconfig")
options := k8s.NewKubectlOptions("", kubeconfig, "production")
// Wait for deployment to be available
k8s.WaitUntilDeploymentAvailable(t, options, "order-api", 30, 10*time.Second)
// Validate pod count
deployment := k8s.GetDeployment(t, options, "order-api")
assert.Equal(t, int32(3), *deployment.Spec.Replicas)
// Validate service
service := k8s.GetService(t, options, "order-api")
assert.Equal(t, int32(8080), service.Spec.Ports[0].Port)
}Get weekly IT automation tips
Docker, Ansible, Terraform, MLOps ā curated insights delivered to your inbox. No spam.
Subscribe Free āTest Structure
modules/
āāā vpc/
ā āāā main.tf
ā āāā variables.tf
ā āāā outputs.tf
āāā web-server/
ā āāā main.tf
ā āāā variables.tf
ā āāā outputs.tf
test/
āāā vpc_test.go
āāā web_server_test.go
āāā go.mod
āāā go.sumCI/CD Integration
# GitHub Actions
jobs:
infrastructure-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- uses: hashicorp/setup-terraform@v3
- name: Run Terratest
working-directory: test/
run: go test -v -timeout 30m -parallel 4
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Best Practices
Use unique names: Append random suffixes to avoid collisions between parallel tests:
uniqueId := random.UniqueId()
terraformOptions := &terraform.Options{
Vars: map[string]interface{}{
"name": fmt.Sprintf("test-%s", uniqueId),
},
}Run tests in parallel: Each test deploys independently:
func TestVpc(t *testing.T) { t.Parallel(); /* ... */ }
func TestWebServer(t *testing.T) { t.Parallel(); /* ... */ }Set timeouts: Infrastructure takes time:
go test -v -timeout 30mAlways defer destroy: Even if assertions fail, cleanup runs:
defer terraform.Destroy(t, terraformOptions)---
Ready to go deeper? Master Terraform testing with hands-on courses at CopyPasteLearn.
Ready to learn by doing?
Stop reading tutorials ā start building. Expert video courses with hands-on labs in real sandboxed environments.
Related Articles
Testkube Kubernetes Testing Guide
Testkube runs tests natively on Kubernetes using any testing framework. Learn how to run integration tests, load tests, and API tests inside your cluster.
Infracost Terraform Cost Estimation
Infracost shows cloud cost estimates for Terraform changes before you apply. Learn how to add cost visibility to pull requests and catch expensive.
Quality vs Cost in DevOps
The quality-cost tradeoff in DevOps is real but misunderstood. Learn why cutting quality to reduce cost usually increases total cost, and how to find.
Terraform Variables and Outputs
Terraform variables and outputs. Types, validation rules, locals, sensitive values, tfvars files, and cross-module dependencies.
Terraform vs CloudFormation vs Pulumi
Compare the top Infrastructure as Code tools ā Terraform, AWS CloudFormation, and Pulumi. Understand their strengths, weaknesses, and ideal use cases.
Terraform Workspaces for Environments
Use Terraform workspaces for dev, staging, and production. Practical patterns, trade-offs, and best practices for multi-env IaC.
Explore topics
Browse more articles on the topics covered here.