
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
You write Terraform, run terraform plan, and hope the apply works. But plans only validate syntax and API schema, not runtime behavior. To truly test infrastructure code with Terratest, you must deploy real resources, assert their actual state via cloud APIs, and tear them down automatically. This guide shows you exactly how to build that validation loop using Go, integrating it into your CI pipeline, and managing the costs of ephemeral test environments.
How do you set up a project to test infrastructure code with Terratest?
Terratest is a Go library, so your test suite lives in standard Go files alongside or adjacent to your Terraform modules. Before writing assertions, you need a properly initialized Go module and the correct directory structure. A common mistake is mixing test code directly into production module directories without clear separation; instead, create a dedicated test/ directory at your repository root or within each module folder.
Initialize the Go test module
Navigate to your test directory and initialize a new Go module. Pin the Terratest version explicitly to avoid breaking changes during CI runs.
mkdir -p test/vpc-module
cd test/vpc-module
go mod init github.com/your-org/infra-tests
go get github.com/gruntwork-io/[email protected]
go get github.com/stretchr/[email protected] Your go.mod should now include both terratest and testify. The latter provides readable assertion helpers that produce better failure messages than raw Go comparisons. For teams adopting infrastructure as code with Terraform, this setup mirrors application testing patterns developers already understand.
Structure test files correctly
Name test files with the _test.go suffix and use descriptive function names prefixed with Test. Each test function should be self-contained: it deploys its own isolated resources, validates them, and destroys them. Never share state between test functions.
// vpc_test.go
package vpc_module
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVpcCreatesCorrectCIDR(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../../modules/vpc",
Vars: map[string]interface{}{
"cidr_block": "10.0.0.0/16",
"env": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
actualCidr := terraform.Output(t, terraformOptions, "vpc_cidr")
assert.Equal(t, "10.0.0.0/16", actualCidr)
} The t.Parallel() call enables concurrent test execution, which is critical because infrastructure tests are inherently slow. Without parallelism, a suite of ten VPC tests could take two hours sequentially versus twenty minutes concurrently. Always pair parallel execution with unique resource naming to prevent collisions.
What assertions actually matter when validating cloud resources?
Static outputs only confirm what Terraform thinks it created. Real validation queries the cloud provider API directly to verify the resource exists with the expected configuration. Terratest provides helper packages for AWS, Azure, GCP, and Kubernetes that wrap SDK calls into test-friendly functions.
Validate beyond Terraform outputs
A frequent anti-pattern is asserting solely on terraform.Output values. This tells you the output was set, not that the resource works. Instead, fetch the resource ID from Terraform, then query the provider API independently.
import (
awsModule "github.com/gruntwork-io/terratest/modules/aws"
)
func TestSecurityGroupAllowsSSH(t *testing.T) {
// ... init and apply ...
sgId := terraform.Output(t, opts, "security_group_id")
region := "us-east-1"
sg := awsModule.GetSecurityGroup(t, region, sgId)
ingressRules := sg.IpPermissions
sshRuleFound := false
for _, rule := range ingressRules {
if *rule.FromPort == 22 && *rule.ToPort == 22 {
sshRuleFound = true
break
}
}
assert.True(t, sshRuleFound, "SG %s missing SSH ingress rule", sgId)
} This approach catches drift between Terraform state and reality. If someone manually modified the security group after deployment, or if the provider API silently ignored a parameter, this test fails where an output-only assertion would pass. For teams implementing DevSecOps practices, these direct API validations serve as automated compliance checks.
Test connectivity and runtime behavior
Infrastructure exists to serve applications. Validate that deployed resources actually function by making HTTP requests, opening TCP connections, or running commands on provisioned instances.
- Use
http_helper.HttpGetWithRetryto validate web endpoints become reachable within a timeout window - Use
ssh.CheckSshCommandto execute validation scripts on remote EC2 instances - Use
aws.GetS3BucketContentsto verify object storage permissions and lifecycle policies - Use retry wrappers for all network assertions since cloud resources take time to stabilize
How do you integrate Terratest into CI pipelines without bankrupting your team?
Infrastructure tests consume real cloud resources and cost real money. Running them on every pull request without guardrails leads to surprise bills and blocked pipelines. You need a tiered strategy that balances feedback speed with validation depth.
Implement staged test execution
Not all tests deserve the same trigger frequency. Categorize your test suite by cost and duration:
| Test Tier | Trigger | Duration | Cost Impact | Examples |
|---|---|---|---|---|
| Unit (Plan) | Every commit | < 30s | $0 | terraform plan validation, OPA policy checks |
| Integration | PR merge to main | 5–15 min | Low ($1–5/run) | Single module deploy + API assert + destroy |
| End-to-End | Nightly / Release tag | 30–60 min | Medium ($10–30/run) | Full stack deploy, cross-module dependencies |
| Compliance Audit | Weekly / Pre-audit | 1–2 hr | Higher | Security controls, SOC 2 evidence collection |
For PR-level feedback, rely on terraform plan parsing and static analysis tools like conftest or checkov. Reserve full Terratest deployments for post-merge validation. This aligns with the testing pyramid principles covered in our test automation strategy guide.
Enforce mandatory cleanup and cost guards
A leaked test resource is a silent budget drain. Every test function must include defer terraform.Destroy(t, opts) immediately after defining options, before InitAndApply. This ensures destruction runs even when assertions fail or tests panic.
func TestEphemeralResources(t *testing.T) {
t.Parallel()
opts := &terraform.Options{
TerraformDir: "../../modules/ephemeral-worker",
NoColor: true,
}
// CRITICAL: defer BEFORE apply, not after
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
// Assertions here...
// Destroy runs automatically on exit, panic, or failure
} Add CI-level safeguards: set maximum test timeouts (go test -timeout 30m), implement billing alerts for test accounts, and tag all test resources with CreatedBy=terratest plus expiration timestamps. Use a separate AWS account or Azure subscription exclusively for testing to isolate blast radius and simplify cost tracking.
When should you choose Terratest over other IaC testing tools?
Terratest isn't the only option, and it's not always the right one. Understanding trade-offs prevents over-engineering your test suite. The decision hinges on what failure mode you're trying to catch and how much latency you can tolerate in your feedback loop.
Use Terratest when you need to verify that resources actually work together at runtime: security groups permit expected traffic, IAM roles grant correct permissions, DNS records resolve properly, or load balancers distribute traffic as configured. These are integration and end-to-end concerns that no linter can validate.
Avoid Terratest for pure syntax checking, policy enforcement, or fast PR feedback. Tools like tfsec, checkov, and tflint run in milliseconds with zero cloud cost. For teams building reusable Terraform modules, combine static analysis on every commit with Terratest validation on merge to main. This layered approach gives you confidence without sacrificing developer velocity.
One practical consideration: Terratest requires Go proficiency. If your team lacks Go experience, the learning curve adds friction. In such cases, consider kitchen-terraform (Ruby-based) or Pulumi's native testing framework if you're already using those ecosystems. Don't force a tool mismatch that slows adoption.
Making Infrastructure Testing Sustainable
To test infrastructure code with Terratest effectively, treat it as a long-term engineering investment, not a checkbox. Start small: pick one critical module, write three meaningful assertions, integrate it into your post-merge pipeline, and measure the value before expanding. Track metrics like escaped defects caught in staging versus production, mean time to detect misconfigurations, and monthly test infrastructure spend.
The goal isn't 100% coverage—it's targeted confidence in the components that break expensively. Pair Terratest with strong secrets management in CI/CD to avoid credential leaks in test logs, and document your test strategy so new engineers understand why certain validations exist.
If your team needs help designing a sustainable infrastructure testing strategy or integrating Terratest into existing compliance workflows, reach out to discuss your specific environment. Getting the foundation right prevents months of rework and keeps your cloud bill predictable while maintaining deployment confidence.