5.7. Testing

Preparation

Create a new directory for this exercise:

mkdir -p $LAB_ROOT/advanced/testing
cd $LAB_ROOT/advanced/testing

Optional: Create empty files:

touch {main,variables,outputs,versions}.tf

Step 5.7.1: Module under test

We will test a small module that generates a formatted greeting string. First, create the module directory and source files:

mkdir -p modules/greeting

Create modules/greeting/variables.tf:

variable "name" {
  description = "Name to greet."
  type        = string
}

variable "language" {
  description = "Language code: 'en' or 'de'."
  type        = string
  default     = "en"

  validation {
    condition     = contains(["en", "de"], var.language)
    error_message = "language must be 'en' or 'de'."
  }
}

Create modules/greeting/outputs.tf:

output "message" {
  description = "Formatted greeting."
  value       = var.language == "de" ? "Hallo, ${var.name}!" : "Hello, ${var.name}!"
}

Create modules/greeting/versions.tf:

terraform {
  required_version = ">= 1.12.0"
}

Call the module from the root main.tf:

module "greeting" {
  source   = "./modules/greeting"
  name     = "Terraform"
  language = "en"
}

And expose the output in outputs.tf:

output "greeting" {
  value = module.greeting.message
}

Create versions.tf:

terraform {
  required_version = "= 1.12.2"
}

Run terraform init and terraform apply to confirm the module works:

terraform init
terraform apply
Outputs:

greeting = "Hello, Terraform!"

Step 5.7.2: Write a test file

terraform test (introduced in Terraform 1.6) reads .tftest.hcl files and runs the assertions defined inside. Each test file can contain multiple run blocks, each of which executes a plan or apply and then checks assert blocks.

Create a directory for tests and a first test file:

mkdir tests

Create tests/greeting.tftest.hcl:

# Test the default English greeting
run "english_greeting" {
  variables {
    name     = "World"
    language = "en"
  }

  module {
    source = "./modules/greeting"
  }

  assert {
    condition     = output.message == "Hello, World!"
    error_message = "Expected English greeting but got: ${output.message}"
  }
}

# Test the German greeting
run "german_greeting" {
  variables {
    name     = "Welt"
    language = "de"
  }

  module {
    source = "./modules/greeting"
  }

  assert {
    condition     = output.message == "Hallo, Welt!"
    error_message = "Expected German greeting but got: ${output.message}"
  }
}

Run the tests:

terraform test
tests/greeting.tftest.hcl... in progress
  run "english_greeting"... pass
  run "german_greeting"... pass
tests/greeting.tftest.hcl... tearing down
tests/greeting.tftest.hcl... pass

Success! 2 passed, 0 failed.

Explanation

Each run block:

PropertyDescription
commandplan (default) or apply – use apply when assert values depend on computed attributes
variablesOverride input variables for this run only
moduleTarget a child module directly instead of the root module
assertOne or more conditions; test fails if any condition is false

By default terraform test uses the plan command, which is fast and does not create real infrastructure. Use command = apply when you need to test computed output values that are only known after apply.

Step 5.7.3: Test for validation errors

You can also verify that Terraform correctly rejects invalid input. Add a second test file to cover the validation block in the language variable:

Create tests/validation.tftest.hcl:

run "invalid_language_is_rejected" {
  variables {
    name     = "Test"
    language = "fr"
  }

  module {
    source = "./modules/greeting"
  }

  # We expect this run to fail with a validation error
  expect_failures = [
    var.language,
  ]
}

Run the tests again:

terraform test
tests/greeting.tftest.hcl... pass
tests/validation.tftest.hcl... in progress
  run "invalid_language_is_rejected"... pass
tests/validation.tftest.hcl... pass

Success! 3 passed, 0 failed.

Explanation

expect_failures accepts a list of resource/variable addresses that are expected to produce a validation error. If the run succeeds (no error), the test itself fails. This pattern lets you write negative tests that confirm your guards are working correctly.