Test Infrastructure Code with Terratest

Khimananda Oli 8 min read Virtualization
Test Infrastructure Code with Terratest

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.

Go Test Runnerterratest.Run()Terraform ApplyReal Cloud DeployCloud ProviderAWS / Azure / GCPAssert & TeardownAPI Check + Destroy
Terratest executes real deployments, validates live infrastructure via provider APIs, and guarantees cleanup through deferred teardown functions.

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.HttpGetWithRetry to validate web endpoints become reachable within a timeout window
  • Use ssh.CheckSshCommand to execute validation scripts on remote EC2 instances
  • Use aws.GetS3BucketContents to verify object storage permissions and lifecycle policies
  • Use retry wrappers for all network assertions since cloud resources take time to stabilize
Weak Assertion Patternassert.Equal(expected, terraform.Output())Only validates state file valueMisses API drift & silent failuresStrong Validation Patternaws.GetResource(id) → assert propsQueries live provider API directlyCatches config drift & runtime issuesRecommended Assertion Layers1. Output ValuesBasic sanity check2. Provider API StateActual resource config3. Runtime BehaviorHTTP/SSH/connectivity
Effective infrastructure testing layers output checks, direct API validation, and runtime behavior assertions to catch different failure modes.

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 TierTriggerDurationCost ImpactExamples
Unit (Plan)Every commit< 30s$0terraform plan validation, OPA policy checks
IntegrationPR merge to main5–15 minLow ($1–5/run)Single module deploy + API assert + destroy
End-to-EndNightly / Release tag30–60 minMedium ($10–30/run)Full stack deploy, cross-module dependencies
Compliance AuditWeekly / Pre-audit1–2 hrHigherSecurity 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.

What Are You Testing?Syntax & PolicyUse tfsec / checkovPlan CorrectnessUse terraform plan + conftestRuntime BehaviorUse Terratest ✓Terratest Best For:• Cross-resource dependencies• Network connectivity validation• IAM permission verification• Multi-module integration• Compliance evidence generationAvoid Terratest When:• Only checking HCL syntax• Budget prohibits real deploys• Feedback needed in < 2 min
Choose Terratest specifically for runtime validation scenarios where static analysis and plan inspection cannot confirm actual infrastructure behavior.

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.

Frequently Asked Questions

Terratest is a Go library that validates Terraform, OpenTofu, and Helm deployments by running real infrastructure and asserting expected states. It catches configuration drift, API changes, and integration failures before production deployment in 2026 environments.

Run go get github.com/gruntwork-io/terratest/modules inside your Go test directory. Ensure Go 1.22+ and Terraform or OpenTofu CLI are installed and accessible in your system PATH for execution.

The library is free and open source, but tests provision real cloud resources. You pay standard provider fees for compute, storage, and networking during test execution. Always implement cleanup hooks to avoid lingering charges.

Yes. Use the helm and k8s modules to install charts into ephemeral namespaces, wait for pod readiness, and validate service endpoints. Tests automatically delete namespaces on completion to prevent cluster resource leakage.

Most suites run between five and twenty minutes depending on resource provisioning time. Parallelize independent test functions using t.Parallel() to reduce total CI pipeline duration without increasing cloud spend significantly.

Validate checks HCL syntax and static schema compliance only. Terratest deploys actual infrastructure and verifies runtime behavior, connectivity, and application functionality that static analysis cannot detect in complex 2026 architectures.

Never hardcode credentials. Inject them via environment variables or CI secret stores. Use terratest/modules/aws/secretsmanager or HashiCorp Vault to fetch temporary credentials at runtime and revoke them immediately after test completion.

Not for real infrastructure validation. Use LocalStack or kind for AWS or Kubernetes emulation during development. These tools simulate APIs but may miss provider-specific behaviors that only live cloud testing reveals accurately.

Always defer terraform.Destroy(t, terraformOptions) immediately after Apply in every test function. This guarantees cleanup even when assertions fail or tests panic, preventing orphaned resources and unexpected monthly cloud bills.

Missing IAM permissions, insufficient quota limits, and incorrect region configuration cause most failures. Verify your test service account has least-privilege access to all required APIs before writing assertions against deployed resources.

Add a job step installing Go and Terraform, then run go test -v -timeout 30m. Configure OIDC federation for cloud authentication instead of long-lived keys to maintain zero-trust security posture in 2026 pipelines.

Yes. Create separate TerraformOptions for each module path and apply them sequentially or in parallel. Pass outputs from upstream modules as input variables to downstream ones to validate end-to-end integration behavior.

Enable verbose logging with -v flag and inspect terraform.Output calls for actual values. Use cloud provider consoles or CLI tools to examine live resource state when test output lacks sufficient diagnostic context.

Yes. Set the BinaryName field in TerraformOptions to tofu instead of terraform. All core modules support OpenTofu natively since version 0.47, enabling vendor-neutral infrastructure testing without code changes.

Prioritize integration tests for critical paths like networking and IAM policies. Reserve unit-style checks for reusable module logic using plan-only assertions to balance coverage, speed, and cloud cost efficiency.