Reference · Terraform

Native Testing (terraform test)

.tftest.hcl anatomy, plan vs apply runs, mocking — the framework, GA since Terraform 1.6.

Anatomy of a test file

# tests/naming.tftest.hcl  (auto-discovered in ./ and ./tests/)
variables {                       # file-level inputs for every run
  env    = "test"
  prefix = "acme"
}

run "bucket_name_is_composed" {  # runs execute IN ORDER, sharing state within the file
  command = plan                  # ⚠ default is apply — creates real infra if omitted!

  variables { env = "dev" }       # per-run override

  assert {
    condition     = aws_s3_bucket.b.bucket == "acme-dev-logs"
    error_message = "bucket name did not compose correctly"
  }
}

run "rejects_bad_env" {           # testing that validation FAILS correctly
  command = plan
  variables { env = "production!!" }
  expect_failures = [var.env]     # the variable's validation block must reject this
}

Run with terraform test. Output is pass/fail per run block.

Sources: Tests reference · official tutorial

The big defaults table

FactDetail
Default commandapply — a bare run block creates real infrastructure. Write command = plan explicitly for unit-style checks.
CleanupApply-mode resources are auto-destroyed when the file finishes (reverse order). A crashed test run can still strand resources — tests get their own throwaway account/prefix.
Orderingrun blocks execute sequentially, sharing state — a later run can reference an earlier run's outputs (run.setup.bucket_arn).
Unknown valuescommand = plan can't assert on computed attributes (ARNs, generated IDs) — they're unknown until apply. Assert on what's known at plan time, or use apply/mocks.
expect_failuresAsserts that a checkable object (variable validation, precondition, check block) fails. Recommended with command = plan.

Mocking (Terraform ≥1.7): no credentials, no infrastructure

mock_provider "aws" {}            # replaces the real provider for this test file

run "policy_logic" {
  command = apply                  # "apply" against the MOCK — nothing real is created
  override_resource {              # pin specific computed values when logic needs them
    target = aws_s3_bucket.b
    values = { arn = "arn:aws:s3:::acme-dev-logs" }
  }
  assert {
    condition     = aws_iam_policy_document.read.json != ""
    error_message = "policy should render"
  }
}

Sources: Provider mocking docs · Terraform 1.7 announcement

The testing pyramid for Terraform

LayerMechanismCostCatches
Staticterraform validate, fmt, lintersSeconds, freeSyntax, type errors, unknown attrs
Unittest with command = plan and/or mocksSeconds, freeNaming, composition, conditionals, validation rules
Integrationtest with real command = applyMinutes, real moneyProvider behaviour, IAM reality, actual wiring
Strategy: static + unit on every push; real-apply integration tests for modules on a schedule or pre-release, in a sandbox account. Same pyramid economics as software testing. For needs beyond the native framework (multi-step orchestration, Go assertions), Terratest is the established heavyweight alternative.