The terraform test Framework
Terraform 1.6 shipped a built-in test framework, and it changed what testing infrastructure code looks like. You write tests in HCL in .tftest.hcl files, each containing run blocks that execute a plan or an apply against your configuration and assert blocks that check the results. No external language, no Go, no separate harness — the tests live next to the module and run with terraform test.
The question this answers is the one every module author has: does this module actually create what it claims, do its variable validations fire when they should, does it behave correctly across the input combinations people will throw at it. Before 1.6 you reached for Terratest in Go or hoped a careful plan review would catch regressions. Now the assertions are first-class, version-controlled HCL that runs in CI on every change to the module.
The Framework
A test file is a sequence of run blocks. Each run sets variables, runs either a plan or an apply, and asserts on outputs and resource attributes. Runs in one file execute in order and share state, so a later run can build on what an earlier one created. The assertion is a condition that must be true and an error_message printed when it is not — the same shape as a variable validation.
run "sets_bucket_name_from_prefix" { command = plan variables { name_prefix = "acme-prod" } assert { condition = aws_s3_bucket.this.bucket == "acme-prod-logs" error_message = "bucket name must be the prefix plus -logs" } }
command = plan makes this run compute a plan and assert against the planned values without creating anything. The test passes if the module would name the bucket acme-prod-logs given that prefix, and fails with a clear message if the naming logic regresses.
Plan vs Apply Tests
Every run chooses command = plan or command = apply, and the difference is the whole cost model. A plan test computes the diff and asserts on planned values — it is fast, free, and creates no real resources, so it verifies the config's intended shape and logic. An apply test actually creates the infrastructure, asserts that it works end to end, then destroys it — which costs time, money, and demands reliable cleanup. Plan tests are the broad base of the pyramid; apply tests are the narrow, expensive top reserved for critical paths.
Mocking Providers
Plan tests still need a provider to compute the plan, and that is where mock providers come in. A mock_provider block stands in for the real AWS provider, returning synthetic values for computed attributes so the plan resolves without real credentials or API calls. This lets you test a module's logic — its conditionals, its for_each shaping, its name construction — entirely offline, with no AWS account and no cost, which is exactly what you want running on every pull request.
mock_provider "aws" {} run "creates_one_subnet_per_az" { command = plan variables { availability_zones = ["us-east-1a", "us-east-1b"] } assert { condition = length(aws_subnet.this) == 2 error_message = "one subnet must be created per availability zone" } }
With the mock provider in place, this run asserts that the module creates exactly one subnet per AZ for any list you pass — pure logic, verified in milliseconds, no infrastructure created. That is the sweet spot for the framework: testing the parts of a module that have logic worth getting wrong.
In CI
Run terraform test on every change to a module, before it is published or merged. Lean on plan-based tests with mock providers for the bulk of coverage, since they are fast enough to run on every commit and cost nothing. Reserve apply-based tests for a handful of critical end-to-end paths where "it plans correctly" is not enough assurance — and make sure those tests clean up reliably even when an assertion fails partway through, or you will leak resources and pay for them.
Plan-based tests (command = plan) — compute the diff and assert on planned values without creating real resources. Fast, free, and safe to run on every commit; with a mock provider they need no AWS account. Use them broadly to verify the config's intended shape, conditionals, and validation logic.
Apply-based tests (command = apply) — create real infrastructure, assert it works end to end, then destroy it. Slow, costing real money, and dependent on reliable cleanup. Reserve them for a few critical paths where only a real apply proves the thing works.
- Writing only apply-based tests that are slow and cost real money, when most of the assertions could run as fast, free plan tests against a mock provider.
- Running apply tests without reliable cleanup, so a failure mid-run leaks real resources and you discover them on the next bill.
- Testing trivial pass-through behavior — that a variable reaches an output unchanged — instead of the conditional logic and validation that actually breaks.
- Assuming the native framework covers everything and skipping contract or integration testing where it genuinely fits better.
- Forgetting that runs in one file share state and execute in order, then being confused when a later run sees resources an earlier run created.
- Favor plan-based tests for speed and cost, reserving apply-based tests for the few critical end-to-end paths that justify creating real resources.
- Use
mock_providerblocks to test module logic with no AWS account, so the suite runs on every commit for free. - Test the parts that have logic — conditionals,
for_eachshaping, variable validation, computed outputs — not trivial pass-through values. - Ensure every apply-based test cleans up reliably, even when an assertion fails partway through the run.
- Run
terraform testin CI on every module change, before publishing a new version.
Knowledge Check
What is the trade-off of a command = apply test versus a command = plan test?
- The apply test creates real infrastructure and verifies it end to end, at the cost of time and cleanup; the plan test is fast and free but only checks planned values
- The apply test runs entirely offline with a mock provider and costs nothing, while the plan test needs real live AWS credentials and provisions resources to check them
- The apply test can only assert on declared outputs while the plan test can assert on any resource attribute
- The plan test creates the real resources while the apply test only previews them as a diff
What does a mock_provider block let you do?
- Run plan-based tests that exercise the module's logic without real credentials, API calls, or created resources
- Apply a module to a real isolated sandbox AWS account that is then automatically created and torn down for you each run
- Replace the test framework's hand-written assert blocks with provider-generated checks
- Skip the plan phase entirely and assert directly against the saved state file
In a module, which behavior is most worth covering with a test?
- The conditional and
for_eachlogic — for example, that one subnet is created per availability zone passed in - That a plain string input variable arrives at a named output unchanged, with no transformation applied to it
- That the AWS provider itself returns the correct ID for a freshly created resource
- That a run of
terraform fmtleaves the module's source files unchanged
Which Terraform version introduced the native .tftest.hcl framework?
- 1.6 — before it, teams reached for external tools like Terratest
- 0.12, alongside the original introduction of the new HCL2 expression syntax
- 1.1, shipped in the very same release as the new moved blocks
- It has always existed as a built-in part of the core CLI
You got correct