Text size
Tried & Tested6 min read

Testing infrastructure code

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

Testing infrastructure code
AI-generated editorial illustration

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.

The testing pyramid for infrastructure

Like application testing, infrastructure testing has layers, from cheap and fast to slow and expensive:

  1. Static checks: formatting, validation, linting, security scanning. Seconds, no cloud needed.
  2. Policy tests: rules that every plan must follow. Seconds, using the plan output.
  3. Unit-style tests: checking module logic against a plan, or with mocked providers. Fast.
  4. Integration tests: actually creating real resources in a test account, checking them, and destroying them. Minutes, costs money.

Most of the value comes from the first two layers. The last one is worth it for important shared modules.

Cheap checks first

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.
  • tflint catches provider-specific mistakes: invalid instance types, deprecated arguments, missing tags.
  • Checkov or Trivy catch security misconfigurations: public buckets, open security groups, missing encryption.

They're fast, free and catch a surprising number of real problems.

The best infrastructure bug is the one caught before terraform plan ever talks to your cloud.

terraform test

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

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.

Policy tests

Policies answer "is this allowed?" for every change, regardless of who wrote it:

  • OPA / Conftest with policies written in Rego, run against the plan in JSON format.
  • Checkov custom policies in Python or YAML.
  • Sentinel in HCP Terraform, or OPA integration in other platforms.

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/

Paying for test environments

Integration tests cost money. Ways to keep it under control:

  • A dedicated test account with budgets and alerts.
  • Always destroy after tests, and run a nightly cleanup job (tools like cloud-nuke or aws-nuke) for anything left behind.
  • Small, cheap resource sizes in test examples.
  • Run integration tests on merge or nightly, not on every commit.
  • Test modules, not whole environments.

So, where do I start?

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