Course outline

Terraform / Sensitive data, governance, and recovery / Lesson 14

Validation, tests, checks, and their execution effects

Choose the appropriate assertion boundary and remember that a Terraform test can perform real planning or application.

About 4 min readContent reviewed
On this page

Lesson goals

  • Compare variable validation, preconditions, postconditions, checks, and test assertions.
  • Distinguish formatting, static validation, mocked tests, plan tests, and integration tests.

Different assertions stop or report at different boundaries

Variable validation checks an input domain. Preconditions and postconditions express supported resource, data, or output contracts at their documented evaluation points. A check block supports continuing validation and reports failed assertions as warnings rather than acting as a universal enforcement gate. Terraform test assertions evaluate expected behavior within test runs. Choose the mechanism from the requirement and failure semantics instead of placing every rule in a check and assuming unsafe changes are blocked. Unknown values can also defer when a condition becomes evaluable.

A pure output contract is easier to reason about

The complete root module below normalizes a service name and attaches a precondition to its output. It contains no managed resources or external data reads, but remains non-runnable editorial HCL because Terraform is not available for validation here. The contract requires the normalized name to start with atlas-. A successful text inspection is not an observed Terraform assertion result. Keep runtime and provider-dependent testing separate from this fixed expression-level example.

hcl Complete expression-level contract
terraform {
  required_version = ">= 1.16.0, < 1.17.0"
}

variable "service_name" {
  type    = string
  default = "atlas-api"
}

locals {
  normalized = lower(var.service_name)
}

output "name" {
  value = local.normalized
  precondition {
    condition     = startswith(local.normalized, "atlas-")
    error_message = "Normalized service names must start with atlas-."
  }
}

The declared default normalizes to atlas-api and satisfies the prefix rule logically. The precondition is an enforcement contract at its supported output boundary, not a warning-only check block. No Terraform command or assertion is executed.

A test file must declare its operation deliberately

Terraform test run blocks default to apply unless configured otherwise. That can create or change infrastructure and requires cleanup and independent authorization. The companion test below explicitly chooses plan and checks a known output from the preceding fixture. Even a plan test can involve providers, backends, or data reads in a different configuration, so the word test does not make an unknown project harmless. This test file is shown only; no test runner, provider installation, or application is performed.

hcl Companion plan test, not executed
run "normalized_name" {
  command = plan
  variables {
    service_name = "ATLAS-API"
  }
  assert {
    condition     = output.name == "atlas-api"
    error_message = "The output must publish the normalized catalogue name."
  }
}

Place this file with the preceding root as tests/name.tftest.hcl. The assertion compares the public output with the declared contract rather than a provider-created id. This is a static teaching example, not a claim of a passing Terraform test.

Mocking changes evidence, not production truth

Supported mock providers and overrides can isolate module behavior from real remote systems, but their schemas and generated values do not certify real API permissions, quotas, readiness, or failure recovery. Plan-only tests, mocked tests, and provider integration tests therefore answer different questions. Define which properties each test can establish and which remain untested. A formatting check can detect style differences; static validation checks configuration consistency under its prerequisites; neither is a substitute for reviewing an actual authorized change plan.

Keep failure cases and execution boundaries explicit

Test stable identities, input boundaries, missing and unknown values, output compatibility, replacement intent, and policy rejection where those belong to the module contract. Avoid tests that merely repeat implementation text without expressing meaningful behavior. Preserve version and dependency evidence with results, and distinguish expected assertion failures from unrelated type or provider errors. Tests should be safe to run only within their approved environment and cleanup model. This course does not invoke validate, test, plan, apply, or any cloud-facing operation.

Common errors and fixes

A warning-only check is expected to block application.

Why it happens
Continuing validation was confused with an enforcement precondition or policy gate.
How to fix it
Choose the documented blocking mechanism appropriate to the requirement.
Prevent it next time
Review assertion severity and evaluation timing explicitly.

A test unexpectedly creates resources.

Why it happens
The default apply operation or provider-dependent behavior was not reviewed.
How to fix it
Stop through the authorized process and reconcile created state and objects.
Prevent it next time
Declare test operation, scope, credentials, and cleanup before execution.

A mocked success is reported as verified production readiness.

Why it happens
Evidence from an isolated contract test was generalized beyond its boundary.
How to fix it
State the tested properties and require separate real-environment acceptance evidence.
Prevent it next time
Label formatting, static, mocked, plan, and integration results separately.

Check your understanding

1. Why does the companion test explicitly say command=plan?

Show worked solution

A test run otherwise defaults to application. The explicit operation communicates the intended verification scope for this fixture. It still does not authorize running arbitrary plan tests, because another configuration can read providers or backends. Review the actual source and execution context before any separately approved test.

2. Can a check warning replace a hard requirement to reject an unsafe input?

Show worked solution

No. Use the appropriate input validation, precondition, or policy enforcement boundary according to the contract. A warning can be useful observability, but treating it as rejection changes the safety model. Document expected failure behavior and test the enforcement path rather than relying on a reassuring message.

Key takeaways

  • Tests can perform real operations; declare their scope and command.
  • Report exactly what formatting, validation, mocking, or integration evidence proves.

Official references