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.