How do you test Terraform code?
Quick Answer
Layered testing: terraform validate and fmt -check catch syntax/style issues fast in CI. terraform plan review (manual or automated diff-checking) catches unexpected resource changes before apply. For real correctness testing, terraform test (built-in, HCL-based) or Terratest (Go) actually apply configuration against real (or LocalStack-mocked) infrastructure, assert on outputs/resource properties, then tear it down, verifying the module truly provisions what it claims to. Static analysis/security scanners (tflint, checkov, tfsec) round this out by catching misconfigurations (open security groups, unencrypted storage) before they ever reach plan.
Detailed Answer
Testing infrastructure code is fundamentally harder than testing application code. There's no fast in-memory unit test equivalent when the thing under test is "did a real cloud API create a real VPC correctly?" Real Terraform testing strategies work in layers, from fast/cheap to slow/thorough.
Layer 1 — static checks (fast, run on every commit)
terraform fmt -check
terraform validate
fmt -check enforces consistent style; validate catches syntax errors and internal inconsistencies (referencing an undeclared variable, type mismatches) without needing any provider credentials or real infrastructure.
Layer 2 — security/best-practice linting
Tools like tflint, tfsec, and checkov statically scan configuration for misconfigurations before anything is ever planned: an S3 bucket without encryption, a security group open to 0.0.0.0/0 on a sensitive port, a resource missing required tags. These catch a large class of real-world mistakes without provisioning anything.
Layer 3 — plan review
terraform plan itself is a form of testing. Reviewing the diff before apply catches "this would delete a resource I didn't expect" style mistakes, especially when automated in CI as a required PR check.
Layer 4 — real integration testing
For genuinely verifying that a module provisions what it claims:
# Using the built-in `terraform test` framework (.tftest.hcl files)
run "creates_vpc_with_expected_cidr" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block did not match expected value"
}
}
Alternatively, Terratest (a Go library) actually applies a module against real (or LocalStack-mocked) infrastructure, asserts on real outputs/resource properties via SDK calls, and tears everything down afterward, genuinely exercising the module end-to-end rather than just checking the plan.
Why the layered approach matters
Static checks and linting run in seconds and catch the majority of common mistakes cheaply. Real apply-based integration tests are slow and cost real cloud resources, so they're reserved for critical, reusable modules rather than run on every single PR. A mature testing strategy runs the cheap checks constantly and the expensive ones on a schedule or before publishing a new module version.