hashicorp/terraform
Native test framework (moduletest)
Active contributors: Liam Cervante, terraform-core team.
Purpose
internal/moduletest/ and the terraform test subcommand let users write tests for Terraform configurations in .tftest.hcl files. A test file is a sequence of run blocks, each of which performs a plan or apply against a (possibly modified) version of the configuration and then asserts conditions on the result. Mock providers let tests run without touching real infrastructure.
The test framework is the second-newest major subsystem in Terraform (behind stacks). Its public surface is the test subcommand; its implementation has its own graph builder and runner.
Directory layout
internal/moduletest/
├── file.go # File — top-level container parsed from a .tftest.hcl
├── opts.go # TestOpts — runner configuration
├── run.go # the per-`run`-block runner
├── status.go, status_string.go # Status enum (pending, running, pass, fail, error, skip)
├── progress.go, progress_string.go # Progress enum used for live display
├── suite.go # Suite — a directory of test files
├── graph/ # graph builder for test runs
├── states/ # in-memory per-run state tracking
└── mocking/ # mock-provider integrationThe CLI sides:
internal/command/test.go— theterraform testcommand.internal/command/test_cleanup.go—terraform test cleanup(experimental subcommand).internal/command/junit/— JUnit XML output for CI.
The configuration parser:
internal/configs/test_file.go— 40 KB; parses.tftest.hcl.internal/configs/mock_provider.go— parsesmock_provider {}blocks.
Key types
| Type | File | Description |
|---|---|---|
moduletest.File |
file.go |
One parsed .tftest.hcl file with its Run blocks and global config. |
moduletest.Run |
run.go |
One run block — an action (plan/apply), optional variables, optional assert blocks, optional mock_provider overrides. |
moduletest.Status |
status.go |
The lifecycle state of a run: pending, running, pass, fail, error, skip. |
moduletest.Suite |
suite.go |
A directory of .tftest.hcl files, run together with shared state. |
mocking.MockProvider |
mocking/ |
Wraps providers.MockProvider with the test framework's metadata. |
How it works
sequenceDiagram
autonumber
participant CLI as command.TestCommand
participant Parser as configs.test_file
participant Suite as moduletest.Suite
participant Run as moduletest.Run
participant Engine as terraform.Context
participant Mock as mocking layer
participant View as views.Test
CLI->>Parser: parse all .tftest.hcl
CLI->>Suite: build suite
Suite->>Run: for each run block
Run->>Mock: install mock providers if requested
Run->>Engine: Plan or Apply
Engine-->>Run: result + diags
Run->>Run: evaluate assert blocks against state/plan
Run->>View: emit per-run progress + final pass/failTest files
A typical .tftest.hcl:
mock_provider "aws" {
mock_resource "aws_instance" {
defaults = {
arn = "arn:aws:fake"
id = "i-fake"
}
}
}
variables {
region = "us-east-1"
}
run "plan_only" {
command = plan
assert {
condition = aws_instance.web.tags.Name == "web"
error_message = "instance name is wrong"
}
}
run "apply_changes" {
command = apply
variables {
instance_count = 2
}
assert {
condition = length(aws_instance.web) == 2
error_message = "expected 2 instances"
}
}The parser produces a configs.TestFile, which the runtime adapts into a moduletest.File.
Run lifecycle
Each run block is its own mini-Terraform invocation:
- Variables are merged: file-level
variables, run-levelvariables, plus environment. - Provider overrides from
mock_providerblocks are applied (the test framework swaps the real provider for an in-process mock). terraform.Contextis constructed.PlanorApplyruns.assert {}blocks are evaluated against the resulting plan or state. Each assertion is a precondition-style HCL expression with a custom error message.- The run records its
Status.
State is not persisted across runs by default — each run starts with the prior run's state in memory only. The terraform test cleanup experimental subcommand handles cleanup of leftover real resources from apply runs that didn't get to destroy.
Mock providers
mocking/ and internal/configs/mock_provider.go together implement deterministic replacements for real providers:
mock_resource "type" {}— declares default values for that resource type's attributes.mock_data "type" {}— same, for data sources.override_resource "type" {}andoverride_data "type" {}— per-test overrides applied to the global mock.
The mock provider's PlanResourceChange and ApplyResourceChange synthesize values consistent with the declared defaults plus whatever the user wrote in configuration. This lets a test exercise plan-side and apply-side logic without ever calling out to a real cloud.
Graph builder
internal/moduletest/graph/ builds a graph for an individual run. Despite the name, this is a small graph: it captures the order in which a run's variables, providers, and overrides need to be resolved before the underlying Terraform module's own engine kicks in.
Output formats
The terraform test command supports human, JSON (-json), and JUnit (-junit-xml=...) outputs. JUnit is implemented in internal/command/junit/ and is the path most CI systems use for test reporting.
Integration points
- Configuration: test files are parsed through
internal/configs/test_file.go; mock-provider blocks throughmock_provider.go. The parsed forms are translated intomoduletesttypes. - Engine: test runs use the regular
terraform.Context. Tests do not replace the engine; they orchestrate it. - Plugin interface: mock providers are
providers.Interfaceinstances injected at engine construction time. - CLI:
internal/command/test.gois the top-level driver; it wires up the suite, the views, and the lifecycle. - HCP Terraform: when run via
cloud {}, tests can be executed remotely with the workspace's saved variables (see cloud).
Entry points for modification
- Adding a new
runblock attribute: edit the parser ininternal/configs/test_file.go, add the field tomoduletest.Run, and update the runner inrun.go. - Adding a new mock pattern: extend
mocking/and update the parser. Be sure to consider both plan-side and apply-side semantics. - Adding a new output format: implement a new view in
internal/command/views/test.goand register it inarguments/test.go. - Changing run isolation rules:
internal/moduletest/states/controls how state is threaded between runs in a file.
For how terraform test plumbs through the CLI, see command package. For the engine that runs underneath, see terraform-core.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.