Article

Testing Terraform: What’s Worth Testing, What Isn’t, and Why Most Terraform Tests Are Just a Waste…

Testing Terraform: What’s Worth Testing, What Isn’t, and Why Most Terraform Tests Are Just a Waste of Time

by Gary Worthington, More Than Monkeys

I once reviewed a module repository with four hundred lines of tests. Beautiful green pipeline. Badge in the README. Every test passed on every commit, and the team was rightly proud of the discipline it represented.

Here’s one of the tests, lightly disguised:

run "bucket_has_correct_name" {
command = plan

variables {
name = "invoices"
}
assert {
condition = aws_s3_bucket.this.bucket == "invoices"
error_message = "bucket name should match input"
}
}

Set the name to “invoices”; assert the name is “invoices”. This test verifies that Terraform’s variable interpolation works — a thing HashiCorp tests rather thoroughly on their end. Three hundred of the four hundred lines were variations on it: pass a value in, assert the value came out. The suite could not fail unless Terraform itself was broken, at which point the team would have bigger news to read than their pipeline.

Two months later a change to that same module took out a production service. The tests stayed green throughout, because the tests weren’t testing anything the team had decided — only things Terraform had promised. Green ticks aren’t the same as safety, and infrastructure testing is where that difference gets expensive.

This is part seven of the series, and it’s about spending your testing effort where it actually buys risk reduction: what terraform test is good at, what it can't see, and how to tell a real test from a ceremonial one.

A quick recap

For newcomers: this series has built up a way of running Terraform — state as the artefact that matters, environments as directories in separate accounts, modules that encode decisions, a pipeline that holds the pen, plus imports and secrets along the way.

Part three matters most today. The argument there was that a module’s job is to encode decisions — your organisation’s opinions about how a service runs, baked into a small interface with strong defaults. Follow that thread one step further and you arrive at what testing means here: if modules encode decisions, then tests exist to verify the decisions, not the tool that executes them.

Keep that sentence. It sorts every Terraform test you’ll ever read into worth-it or worthless.

You can’t unit test a description

Worth pausing on why infrastructure testing feels awkward, because it isn’t your imagination. Terraform code is declarative — mostly nouns, not verbs. There’s no algorithm to probe with edge cases, no function that might return the wrong number on a leap year. In the trivial case, the code is the expected output, which is why so many suites collapse into asserting that inputs arrive where they were sent.

But modules aren’t all nouns. The moment a module contains a conditional, a for_each over transformed data, a validation rule, or a naming convention assembled from parts, it contains logic — decisions with more than one possible outcome. That's the testable surface. A module with no logic needs no tests; a module with logic needs tests for exactly that logic and nothing else.

Which, conveniently, is also a code review heuristic: if you can’t find anything in a module worth testing, ask part three’s question — what decision does this module encode? Sometimes the honest answer is none, and the module shouldn’t exist.

The tests you already have

Before writing any new ones, notice what’s already running. terraform fmt -check and terraform validate are the free tier — syntax, types, references — and belong in the earliest seconds of the pipeline where they fail fast and cost nothing.

And then there’s the plan. Part four put a plan on every pull request and called it the review that matters; it’s also, quietly, the most effective test in the entire toolchain. A plan checks your change against the provider’s schema, against state, against reality, and produces a specific, readable prediction of consequences. Most of what teams hope a test suite will catch — the accidental resource replacement, the security group rule that vanishes — is sitting right there in the plan output, already computed, waiting to be read by someone who doesn’t scroll past it.

Automated tests supplement the plan. Nothing replaces it.

terraform test, and what it’s actually for

Terraform has had a native test framework since 1.6 — files ending .tftest.hcl, containing run blocks that execute the module with chosen variables and assert against the results. No Go, no external harness; the same language as the code under test.

Here’s a test that earns its keep, against the ecs-service module from part three — which, you'll remember, validates that cpu is a real Fargate size:

run "rejects_invalid_cpu" {
command = plan

variables {
name = "orders"
cpu = 300
}
expect_failures = [var.cpu]
}

This is a real test of a real decision. The team decided invalid sizes should die at plan time with a helpful message; this verifies the guard actually fires. If someone refactors the validation away, this fails, which is precisely the job. Note command = plan — the test runs the planning engine only, no resources created, no bill, seconds not minutes. Plan-mode tests are the workhorse: fast enough for every PR, real enough to exercise the module's logic end to end.

The same shape covers the other decisions worth guarding. Does setting environment = "dev" actually produce one instance rather than six — does the branch branch? Does the naming convention hold when the inputs get weird — hyphens, long names, an environment nobody tried before? Does the module's output contract stay stable for the other modules composed against it? Logic, branches, contracts, guards. Yours, not HashiCorp's.

Mocks: fast, cheap, and slightly delusional

Since 1.7 the framework can mock providers: a mock_provider block (with shared data in .tfmock.hcl files if you like) makes every resource pretend-create instantly, computed attributes filled with plausible nonsense. Combined with apply-mode runs, this lets tests exercise sequences that plan alone can't reach, without touching AWS or a credit card.

Use mocks for exactly what they are: a faster way to test your logic. And hold onto the caveat in the name. A mocked apply proves your wiring is coherent; it proves nothing about the world. I watched a team ship an IAM module with a thorough mocked suite — every test green, every ARN in every assertion a syntactically perfect fake minted by the mock. The module composed policies beautifully. The policies themselves, once real, denied the service access to its own queue, because whether IAM grants what you meant is a fact about AWS, not about HCL, and no mock has an opinion about it.

The rule: mocks test that you wired the decision correctly; only reality tests whether the decision works.

Reality-mode tests, spent like money

Which brings us to apply-mode tests against real providers — terraform test creating actual resources, asserting against actual attributes, destroying them after. These are the only tests that catch the reality-shaped failures: IAM semantics, networking that routes on paper but not in packets, provider defaults that changed under you in an upgrade.

They’re also slow, they cost actual pounds, and they can leave debris behind when they fail mid-run. So spend them like money, not like confetti. Reserve them for the modules where reality bites hardest — IAM, networking, anything stateful. Run them nightly or pre-release rather than per-PR, from a dedicated sandbox account (part two’s account structure, earning its keep yet again) where leaked test debris can be bulldozed by a cleanup job without ceremony. A handful of reality tests on the dangerous modules beats a thousand mocked assertions on the safe ones.

The guardrail layer

One more layer, orthogonal to all of the above: static analysis. Tools like Checkov and Trivy scan configuration for known-bad patterns — public buckets, 0.0.0.0/0 ingress, unencrypted storage — and policy-as-code (OPA and friends) lets you encode organisation rules like "every resource carries a cost-centre tag" as machine-checked law rather than wiki folklore.

These aren’t tests of your logic; they’re a floor under everyone’s. They know nothing about what you’re building and everything about what nobody should build. Cheap, fast, occasionally noisy — tune the rules rather than ignoring the tool — and they slot into the pipeline right after validate, where they catch the embarrassing category of incident before it reaches a human reviewer who might be having a distracted Tuesday.

The bullshit, named

For completeness, the suite anti-patterns, so you can spot them in review. Pass-through assertions — input in, same value out — test the interpolation engine; delete them without guilt. Plan snapshot tests, which diff entire plan output against a stored copy, fail on every provider upgrade and cosmetic change until the team learns to update the snapshot without reading it, which is a training programme for ignoring alarms. And coverage mandates imported from application development produce exactly what they produce everywhere: the four-hundred-line suite from the opening, optimised for the metric, useless for the mission. A module with one validation rule needs one test. The rest is set dressing.

None of this is an argument against testing. It’s an argument against pretending — the same argument this series keeps making about plans nobody reads and rituals nobody questions. A small suite that tests decisions is a safety mechanism. A large suite that tests Terraform is a prop.

The test that actually matters

Step back and look at what the series has assembled, because it turns out you’ve been building a test harness all along. Every change is planned against every environment before merge — that’s a test. Applies run in environment order, dev soaking before prod — staged rollout, also a test. The nightly plan says No changes against reality every morning — a continuously running assertion that the code and the world still agree, which is the single most valuable automated check in this entire discipline.

terraform test adds one more layer to that stack: fast, cheap verification that your modules' decisions hold before a plan is ever cut. Use it for that, size it to the logic you actually have, spend reality-mode tests on the modules that can hurt you, and let the guardrail tools hold the floor.

Test your decisions. Terraform already tests itself — and it doesn’t need four hundred lines of your pipeline’s time to do it.

Gary Worthington is a software engineer, delivery consultant, and fractional CTO who helps teams move fast, learn faster, and scale when it matters. He writes about modern engineering, product thinking, and helping teams ship things that matter.

Through his consultancy, More Than Monkeys, Gary helps startups and scaleups improve how they build software — from tech strategy and agile delivery to product validation and team development.

Visit morethanmonkeys.co.uk to learn how we can help you build better, faster.

Follow Gary on LinkedIn for practical insights into engineering leadership, agile delivery, and team performance.