Testing infrastructure code
We test application code. Why not infrastructure? The tools and the trade-offs.
We test application code. Why not infrastructure? The tools and the trade-offs.

Ask a developer whether they'd merge application code without tests, and they'll look at you strangely. Ask the same about Terraform, and many teams shrug: "we run plan and look at it."
Reading a plan is useful, but it's not the same as testing. It doesn't show if the module works as intended, if a change breaks something else, or if security rules are followed. Infrastructure code needs tests too, just a different kind.
Like application testing, infrastructure testing has layers, from cheap and fast to slow and expensive:
Most of the value comes from the first two layers. The last one is worth it for important shared modules.
These tests should run with every commit, preferably as pre-commit hooks and in CI:
terraform fmt -check -recursive
terraform validate
tflint --recursive
checkov -d . # or: trivy config .
fmt and validate catch syntax and formatting issues.They're fast, free and catch a surprising number of real problems.
The best infrastructure bug is the one caught before
terraform planever talks to your cloud.
Since version 1.6, Terraform has a built-in testing framework (OpenTofu has an equivalent with tofu test). Tests are written in HCL, in .tftest.hcl files:
# tests/bucket.tftest.hcl
variables {
name = "acme-test-logs"
environment = "staging"
}
run "bucket_is_private_and_encrypted" {
command = plan
assert {
condition = aws_s3_bucket_public_access_block.this.block_public_acls == true
error_message = "Bucket must block public ACLs."
}
assert {
condition = aws_s3_bucket.this.tags["environment"] == "staging"
error_message = "Bucket must be tagged with its environment."
}
}
With command = plan, tests run against the plan, without creating anything. With command = apply, they create real resources and destroy them afterwards. You can also mock providers, which makes unit-style tests fast and free.
For module authors, this is the easiest way to start testing. No new language, no extra tools.
Terratest, from Gruntwork, is a Go library for integration testing. It deploys real infrastructure, runs checks against it (HTTP requests, SSH commands, API calls), and tears it down.
func TestWebModule(t *testing.T) {
opts := &terraform.Options{TerraformDir: "../examples/web"}
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
url := terraform.Output(t, opts, "url")
http_helper.HttpGetWithRetry(t, url, nil, 200, "OK", 30, 10*time.Second)
}
It's powerful because it tests whether the infrastructure actually works, not just whether it's configured correctly. It's also slower, needs Go knowledge, and costs real money for every run.
Policies answer "is this allowed?" for every change, regardless of who wrote it:
Example rules: no public S3 buckets, all resources must have an owner tag, only approved instance types, production databases must have deletion protection.
terraform plan -out=plan.out
terraform show -json plan.out > plan.json
conftest test plan.json --policy policies/
Integration tests cost money. Ways to keep it under control:
Add static checks to CI today. Write a few terraform test assertions for your most-used module. Add one or two policies for your most important rules. Only add Terratest when a module is critical enough to justify the time and cost.
My take: infrastructure fails in expensive ways. A few cheap tests now save a very expensive lesson later.
— Desly