Lesson 0007 · Understand Terraform

Testing Terraform

One idea · ~14 min. The native test framework — and knowing what each layer of the pyramid can and cannot see.
Why this next? "How to test it" was the first thing you said you wanted from this course. Until Terraform 1.6 (Oct 2023), real testing meant external tools like Terratest; now it's built in — .tftest.hcl files and terraform test. You have all the prerequisites: plan vs apply (lessons 1, 6), unknown-until-apply values, and modules as the unit worth testing (lesson 4).

A test file is HCL: run blocks that execute your configuration and assert on the result. The entire skill of Terraform testing is knowing what each mode can see: plan-mode sees logic, apply-mode sees reality, mocks see your wiring but fake the world.

The shape of a test

# tests/naming.tftest.hcl — discovered automatically by `terraform test`
variables { prefix = "acme" }      # inputs for every run in this file

run "name_composes" {
  command = plan                   # unit-style: fast, free, nothing created
  variables { env = "dev" }
  assert {
    condition     = aws_s3_bucket.b.bucket == "acme-dev-logs"
    error_message = "name did not compose"
  }
}

run "rejects_bad_env" {
  command = plan
  variables { env = "prod!!" }
  expect_failures = [var.env]      # asserts the validation REJECTS this
}

run blocks execute in order, sharing state within a file — later runs can read earlier runs' outputs (run.setup.bucket_arn): setup → exercise → assert, in pure HCL.

The default is apply. A run block without command = plan executes a full, real apply — real resources, real money, real IAM. Terraform auto-destroys them when the file finishes, but a crashed test run can strand them. Treat a bare run block in review exactly like an apply in review.

Mocks (≥1.7): apply without a world

mock_provider "aws" {} swaps the real provider for a fake: command = apply then "creates" resources instantly, with generated placeholder values for computed attributes (ARNs become random strings). Pin the ones your logic needs with override_resource / override_data.

What mocks test: your composition, conditionals, and wiring. What they can't test: whether AWS accepts it. A mocked test can pass while the real apply fails on a real IAM policy.

That gives you a pyramid: static (validate, fmt — seconds) → unit (plan-mode + mocks — seconds, every push) → integration (real apply in a sandbox account — minutes and money, nightly or pre-release). Same economics as software testing.

You're writing the test suite — you call it

✅ Prove it on your own machine (optional, 10 min — no AWS needed)

  1. Write a tiny module: an input env with a validation block (allow only dev/stage/prod), and a local that composes "acme-${var.env}-logs" into a terraform_data (or mocked aws_s3_bucket) name.
  2. Add tests/naming.tftest.hcl with the two runs from above (adapted), and run terraform test.
  3. Break the naming logic on purpose — watch the assert fail with your error message.
  4. If on TF ≥1.7: add mock_provider "aws" {} and try a command = apply run with no credentials.
💬 Good follow-ups: "Show me a realistic test file for the VPC module from lesson 4." · "When do I still need Terratest?" · "How do teams wire terraform test into CI without AWS credentials?"