Open-Source Wikis

/

Terraform

/

Systems

/

Native test framework (`moduletest`)

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 integration

The CLI sides:

The configuration parser:

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/fail

Test 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:

  1. Variables are merged: file-level variables, run-level variables, plus environment.
  2. Provider overrides from mock_provider blocks are applied (the test framework swaps the real provider for an in-process mock).
  3. terraform.Context is constructed.
  4. Plan or Apply runs.
  5. assert {} blocks are evaluated against the resulting plan or state. Each assertion is a precondition-style HCL expression with a custom error message.
  6. 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" {} and override_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 through mock_provider.go. The parsed forms are translated into moduletest types.
  • Engine: test runs use the regular terraform.Context. Tests do not replace the engine; they orchestrate it.
  • Plugin interface: mock providers are providers.Interface instances injected at engine construction time.
  • CLI: internal/command/test.go is 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 run block attribute: edit the parser in internal/configs/test_file.go, add the field to moduletest.Run, and update the runner in run.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.go and register it in arguments/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.

Native test framework (`moduletest`) – Terraform wiki | Factory