How do you test Terraform code?

5 minadvancedterraformtestingterratest

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.

Related Resources