Testing Terraform: From Validate to Native Tests
Infrastructure code deserves tests too. Here's the layered approach I use — fmt, validate, policy checks, and native terraform test — to catch failures before apply.
- #terraform
- #testing
- #validation
- #policy
- #ci
- #devops
For years, “testing” Terraform meant running apply in a sandbox and eyeballing whether anything broke. That’s not testing — that’s hoping. As estates grew and modules got reused across dozens of teams, I needed real confidence that a change wouldn’t break callers. With the terraform test framework now native, there’s no excuse to skip it. Here’s the layered testing approach I run on every module.
Think in layers, cheapest first
Good test strategy runs the fast, cheap checks first and the expensive ones last:
- Formatting —
terraform fmt -check - Validation —
terraform validate - Static policy — security and convention scanning
- Plan-time tests —
terraform testagainst a plan - Apply-time tests —
terraform testagainst real (sandbox) resources
Each layer catches a different class of bug, and the early ones cost milliseconds. Run them in that order so a misformatted file never wastes a sandbox apply.
Layer 1 and 2: fmt and validate
These are non-negotiable and belong in CI on every PR:
terraform fmt -check -recursive
terraform validate
fmt -check keeps the codebase consistent. validate catches syntax errors, bad references, and type mismatches without touching any cloud API. They’re nearly free and catch a surprising share of mistakes.
Layer 3: static policy and security scanning
Before any plan, scan the HCL for security and convention violations with a tool like tfsec, checkov, or an OPA/Conftest policy set:
tfsec .
This catches public S3 buckets, unencrypted volumes, and overly broad security groups as a code-quality gate — no credentials required. It’s the cheapest way to enforce “never ship a 0.0.0.0/0 SSH rule” across an org.
Layer 4 and 5: native terraform test
The terraform test framework lets you write actual test cases in .tftest.hcl files. A plan-mode test asserts on the planned values without creating anything:
run "bucket_is_encrypted" {
command = plan
assert {
condition = aws_s3_bucket_server_side_encryption_configuration.this != null
error_message = "Bucket must have encryption configured."
}
}
command = plan runs fast and never provisions. For deeper confidence — verifying the resource actually comes up and behaves — use command = apply against a sandbox, which creates real resources, asserts, and tears them down automatically:
run "bucket_actually_created" {
command = apply
assert {
condition = output.bucket_arn != ""
error_message = "Expected a non-empty bucket ARN."
}
}
I gate module releases on plan-mode tests for every PR and run the slower apply-mode tests on a schedule or before tagging a release.
Test the module’s contract
What’s worth asserting? The promises your module makes:
- Security defaults hold — encryption on, public access off.
- Required outputs are populated — callers depend on them.
- Input validation rejects bad values — pass a bad
environmentand expect failure.
You’re testing the module’s contract, not re-testing the cloud provider. Keep assertions focused on what you guarantee to callers.
Where AI helps you write tests
Writing test cases is the kind of thoughtful-but-tedious work AI is good at. I paste a module and ask: “Generate terraform test assertions covering the security defaults and every output.” It produces a solid first set of run blocks that I then sharpen. It’s also good at suggesting the edge cases I’d forget — “what happens if this optional list is empty?”
I keep test-generation Terraform prompts for this and run the resulting test files through our Code Review tool to make sure the assertions are meaningful, not just present. A test that always passes is worse than no test, and AI will happily write those if you don’t review them.
Wire it into CI
The whole stack belongs in the pipeline:
- run: terraform fmt -check -recursive
- run: terraform validate
- run: tfsec .
- run: terraform test
Fast checks fail the build in seconds; the native tests give real behavioral confidence. Every PR gets the full ladder.
The takeaway
Terraform code earns the same testing discipline as application code. Layer it: fmt and validate for free, static policy scanning for security, and native terraform test for plan- and apply-time assertions on your module’s contract. Run cheapest-first in CI, test the promises you make to callers, and you’ll catch infrastructure bugs at PR time instead of at 2am.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.