linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/Testing/tf-test-framework

kb/testing ── Testing ── intermediate

Native test framework: .tftest.hcl, run, and assert

Since version 1.6, Terraform ships a built-in test runner. Files named `*.tftest.hcl` describe scenarios through `run` blocks (each a mini plan or apply) and `assert` checks. The `terraform test` command runs all of them and reports pass/fail. No cloud account is required: with `command = plan` the runner evaluates expressions against plan output and creates no resources.

view as markdownaka: terraform-test, tftest-hcl, terraform-native-tests

Why a built-in test runner

Before 1.6, "Terraform tests" meant one of two things: terratest in Go (spin up, verify, tear down) or nothing at all. The first option is expensive in time; the second was common practice. HashiCorp acknowledged the gap and built a runner directly into the CLI.

The idea is straightforward. You place a *.tftest.hcl file next to your module. In it you describe scenarios: which variables to pass in and what the output should look like. terraform test runs each scenario and reports results.

Tests are most valuable for modules. They work at the root level too, but the root configuration is effectively tested by production.

A minimal test

Module layout:

modules/s3-bucket/
├── main.tf
├── variables.tf
└── outputs.tf
tests/
└── basic.tftest.hcl

tests/basic.tftest.hcl:

hcl
variables {
  name = "test-bucket"
}
run "naming" {
  command = plan
  assert {
    condition     = aws_s3_bucket.this.bucket == "test-bucket"
    error_message = "bucket name not propagated from var.name"
  }
}
run "tags" {
  command = plan
  variables {
    tags = { Owner = "ci" }
  }
  assert {
    condition     = aws_s3_bucket.this.tags["Owner"] == "ci"
    error_message = "Owner tag missing"
  }
}

Running it:

bash
terraform test
# tests/basic.tftest.hcl... in progress
#   run "naming"... pass
#   run "tags"... pass
# Success! 2 passed, 0 failed.

No cloud account needed. command = plan stops at the plan stage.

File structure

BlockWhat it does
variables { ... } (file-level)Default variables for all run blocks in the file.
run "<name>" { ... }One scenario. Has its own variables, command, and assert blocks.
assert { condition error_message }A check. condition is a boolean expression. error_message is shown on failure.
provider "X" { ... }Overrides the provider for this file only.
module { source = "..." } (inside run)Uses a different module instead of the one in the current directory. Useful when testing a wrapper.

command = plan vs command = apply

ModeWhat it doesWhen to use
command = planPlan only, no resources created. Attributes that Terraform can resolve at plan stage are available.Unit tests for modules, which covers around 90% of cases.
command = applyFull apply followed by destroy. Computed attributes (ARN, ID) are available.Integration: verify that the module actually works on LocalStack or AWS.

Apply-mode tests are expensive. Each run block triggers an apply and then a destroy. A suite of 10 such tests takes minutes on LocalStack and tens of minutes on AWS. Use them sparingly.

Accessing attributes

Inside assert you can reference:

hcl
# a resource attribute
aws_s3_bucket.this.bucket
# a module output
output.bucket_arn
# a variable
var.name
# computed expressions
length(aws_s3_bucket.this.tags) >= 1
contains(keys(aws_s3_bucket.this.tags), "Owner")

Attributes that are only known after apply (such as arn and id on certain resources) show up as (known after apply) with command = plan. If an assert depends on them, you need command = apply.

Expected failure: testing validation

Sometimes you need to confirm that a plan fails on bad input. Use expect_failures for that:

hcl
run "rejects_empty_name" {
  command = plan
  variables {
    name = ""
  }
  expect_failures = [
    var.name,  # the variable whose validation block must fire
  ]
}

The test passes only if the referenced validation { condition = ... } blocks or preconditions/postconditions actually fail. This is an inverted test.

Testing multiple scenarios for one module

A typical layout:

tests/
├── defaults.tftest.hcl    # default variables, basic happy path
├── tags.tftest.hcl         # tag variations
├── versioning.tftest.hcl   # versioning_enabled / disabled
└── invalid.tftest.hcl      # expect_failures

Each file is an independent scenario. Within a file, run blocks execute sequentially, and each block sees the state left by the previous one when you use command = apply. This is useful for chains: create, verify, update, verify again.

Running a single file

bash
terraform test -filter=tests/tags.tftest.hcl

Or several at once:

bash
terraform test -filter=tests/tags.tftest.hcl -filter=tests/versioning.tftest.hcl

Pitfalls

  • The test framework requires terraform init in the module directory. If you skip it, the runner will print Module not installed. In CI, always run terraform init -backend=false before terraform test.

  • command = plan does not resolve everything. ARNs, IDs, and other computed attributes come back as unknown. If an assert reads aws_iam_role.this.arn, the test will fail with "cannot proceed: value is not known". Either switch to command = apply or check a different attribute.

  • A *.tftest.hcl file is not parsed like a regular .tf file. It is a separate HCL subset with a different set of blocks. You cannot declare resource or data blocks in a test file; those must live in the ordinary .tf files that the test invokes.

  • Provider configuration in test files does not carry over automatically. If your module's provider "aws" block points to a LocalStack endpoint, repeat that block in the *.tftest.hcl file. Otherwise the runner picks up the default provider (without the endpoint) and calls the real AWS. See tf-test-mocks for mock_provider, which solves this class of problems more cleanly.

  • Tests run in your working directory. Not in a separate workspace and not against a separate state. If you use command = apply and the test fails between apply and destroy, those resources remain. Run tests in a clean directory or against LocalStack.

  • The test runner is separate from unit tests in Pulumi or CDKTF. If you have moved to CDKTF, .tftest.hcl files are of limited use. Write your tests in Jest or pytest, and run terraform test only if you want to validate the generated HCL. See tf-cdktf.

§ команды

bash
terraform test

Runs all *.tftest.hcl files. Exit code 0 means all passed; exit code 1 means at least one failed.

bash
terraform test -filter=tests/foo.tftest.hcl

Runs only the specified file. Handy when you are developing a single test.

bash
terraform test -verbose

Prints the plan output for each run block. Useful when you cannot tell why an assert failed.

bash
terraform init && terraform test

The canonical CI step. Without init the runner cannot find the providers.

§ см. также

  • tf-test-mocksMock providers: mock_provider, override_resource, override_dataA mock provider replaces a real AWS provider with synthetic responses. Tests run without the cloud, in seconds rather than minutes. Declare one in `*.tftest.hcl` with `mock_provider "aws"`. To substitute a single resource or data source, use `override_resource` or `override_data`. Without mocks, every `command = apply` block requires LocalStack.
  • iac-testing-theoryWhat to Test in Terraform, and What to SkipInfrastructure is not an application, so do not apply the test pyramid literally. Test module contracts, business rules, complex expressions, and refactors that should produce no destroy. Do not test that the provider works, that the AWS API returns 200, or that a trivial `name = var.name` holds. The goal is to catch regressions, not to prove correctness.
  • tf-module-basicsModule: a reusable piece of infrastructureA module is any directory that contains `.tf` files and can be referenced via a `module` block. The root module is the directory where you run terraform. Child modules are the ones being called. The module contract: input variables (what it accepts), output values (what it exposes). Everything else is an implementation detail.
  • tf-validateterraform validate: checking HCL without the cloud`terraform validate` checks HCL for syntax errors and basic logic issues: unknown arguments, wrong types, and references to resources that do not exist. It does not contact the cloud and does not touch state, so it runs fast. In CI, run it after `init -backend=false` and before `plan`.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies