Skip to content

Testing Modules

terraform validate, terraform plan, and terraform test each check a genuinely different thing — syntax, a real-world diff, and actual asserted behavior — and passing one does not mean the next would also pass.

It is tempting to treat validate, plan, and test as three ways of asking the same question, but they check three different things, and each one catches a class of mistake the others miss entirely.

terraform validate checks that your HCL is syntactically well-formed and internally consistent — that every variable referenced actually exists, that types line up, that required arguments are present. Crucially, validate never talks to GCP at all. It has no credentials, makes no API calls, and cannot tell you whether a project_id is real or whether your account has permission to create a google_storage_bucket.

Terminal window
terraform validate
# Success! The configuration is valid.

terraform plan goes further: it authenticates to the real GCP API, reads the current state of any existing resources, and computes a diff of what would change. This catches an entire category of error validate cannot — an invalid location value the API rejects, a project_id your credentials cannot access, a quota limit. But plan still does not assert anything about whether the result is correct by your own definition; it only shows you what would happen.

Terminal window
terraform plan
# google_storage_bucket.this will be created
# + name = "acme-app-logs"
# + location = "US"

terraform test is the only one of the three that actively asserts specific expected behavior — that the bucket name really is what you passed in, that a given output really has the value you expect. Passing validate, and even a clean plan, tells you the configuration is well-formed and GCP accepted it; neither tells you the module actually does what you designed it to do.

Terraform’s built-in test framework, introduced in Terraform 1.6, runs .tftest.hcl files through the terraform test command. A test file has a top-level variables block for test-specific input values, and one or more run blocks, each containing one or more assert blocks with a condition expression and an error_message shown if that condition is false:

tests/gcs_bucket.tftest.hcl
variables {
bucket_name = "acme-app-logs-test"
project_id = "acme-app-test"
}
run "bucket_name_matches_input" {
command = plan
assert {
condition = google_storage_bucket.this.name == var.bucket_name
error_message = "Bucket name did not match the bucket_name input variable"
}
}

This tests the gcs-bucket module from earlier in this module: it asserts that the bucket resource inside the module ends up with exactly the name the caller passed in as bucket_name, not just that the configuration is syntactically valid or that GCP would accept it.

Every run block has a command argument, and the choice between its two values is a real tradeoff, not a formality. command = plan computes a plan and asserts against the planned values, without creating a single real resource. It is fast, free, and safe to run on every commit, which is why it should cover the majority of your test cases:

run "bucket_name_matches_input" {
command = plan
assert {
condition = google_storage_bucket.this.name == var.bucket_name
error_message = "Bucket name did not match the bucket_name input variable"
}
}

command = apply actually creates real resources in GCP, asserts against their real resulting attributes, and then tears everything down again at the end of the test file. This is genuine integration testing — it can catch things a plan-only test cannot, like a naming collision the GCP API only rejects at apply time — but it is slower and briefly costs real cloud resources, so reach for it deliberately rather than by default:

run "bucket_actually_created" {
command = apply
assert {
condition = google_storage_bucket.this.url != ""
error_message = "Bucket was not actually created in GCP"
}
}
flowchart LR
  v["terraform validate: HCL syntax and internal consistency only"] --> p["terraform plan: talks to the real GCP API, shows the diff"] --> t["terraform test: asserts specific expected behavior via .tftest.hcl"]
Three levels of confidence: terraform validate, terraform plan, and terraform test
What can terraform validate check, and what can it never check
In a tftest.hcl run block, what is the difference between command = plan and command = apply
Why does passing terraform validate not mean a module actually works against real GCP
What does an assert block inside a run block require