TutorialsTerraformCommands › terraform test

Terraform Commands · Command 8 of 8

terraform test

Runs automated test files that assert specific facts about the configuration — using a mock Azure provider, so nothing here ever creates, reads, or touches a real Azure resource. It's how this project catches a broken assumption in the .tf files before anyone ever runs a real plan or apply.

Codey the Sr Developer, standing with a pointer, ready to walk through the guide
Touches real Azure resources → no (mock provider) Test files → *.tftest.hcl Location → terraform/tests/, terraform/bootstrap/tests/

What this command does in detail

Terraform's native test framework looks for *.tftest.hcl files (by default in a tests/ subfolder) and runs each run block inside them in order. Each run block behaves like a miniature plan or apply against the configuration, then checks one or more assert conditions against the result — if a condition is false, that run fails and prints its error_message.

The trick that keeps it offlinemock_provider
terraform/tests/infrastructure.tftest.hcl# Uses a mock provider: even the apply below never creates Azure resources.
mock_provider "azurerm" {
  mock_resource "azurerm_postgresql_flexible_server" {
    defaults = {
      id   = "/subscriptions/.../flexibleServers/test-pg"
      fqdn = "test-pg.postgres.database.azure.com"
    }
  }
  mock_resource "azurerm_container_registry" {
    defaults = {
      id           = "/subscriptions/.../registries/test-acr"
      login_server = "testacr.azurecr.io"
    }
  }
  # ...more mocked resource types
}

mock_provider stands in for the real azurerm provider, returning fake but realistic values (like a made-up server ID and hostname) instead of calling Azure. Even a test run block using command = apply is completely safe — it “applies” only against this fake provider.

Where to run it from working directory

terraform -chdir=terraform test
terraform -chdir=terraform/bootstrap test

Run separately for each root module — this project has test files for both the main configuration and bootstrap/main.tf.

Precondition what must already be true

  • terraform init has run in this folder — -backend=false is enough, since tests use the mock provider and never touch real state.
  • Test files exist under tests/ (Terraform's default location) using the .tftest.hcl extension.
  • Each test file supplies its own variables block with values for anything the configuration requires — a test doesn't read your real terraform.tfvars or TF_VAR_* environment variables; it needs its own.

Postcondition what becomes true after it succeeds

  • No real Azure resources or real state are affected at all — everything ran against the mock provider.
  • Each run block is reported as passed or failed, with a summary at the end.
  • A passing test suite means the configuration's structure and computed values match the assumptions the tests encode — it does not confirm Azure will actually accept those values (name availability, quota, and live permissions are still only proven by a real plan/apply).

How this project uses it real examples

What the tests actually checkterraform/tests/infrastructure.tftest.hcl
run "matches_powershell_resources" {
  command = apply

  assert {
    condition     = azurerm_postgresql_flexible_server.metals.sku_name == "B_Standard_B1ms"
      && azurerm_postgresql_flexible_server.metals.storage_mb == 32768
      && azurerm_postgresql_flexible_server.metals.version == "16"
    error_message = "Database sizing and version must match the PowerShell demo."
  }
  assert {
    condition     = azurerm_linux_web_app.api.site_config[0].application_stack[0].docker_image_name == "metals-api:latest"
      && azurerm_linux_web_app.api.app_settings["WEBSITES_PORT"] == "5000"
    error_message = "The API app must run the metals-api container on port 5000."
  }
  assert {
    condition     = azurerm_linux_web_app.ui.app_settings["API_UPSTREAM"] == "https://${azurerm_linux_web_app.api.default_hostname}"
    error_message = "The UI app must proxy to the API app's actual hostname."
  }
  # ...more assertions, covering the firewall rule, OIDC subject, and role assignments
}

These assertions catch exactly the kind of mistake that's easy to introduce while editing database.tf, app_service.tf, or github_oidc.tf — a web app pointed at the wrong image or port, or the UI's API_UPSTREAM hardcoded to a hostname instead of referencing the API resource. Both are silent in a plan and obvious in a test.

Alongside a separate Python test suite.github/workflows/terraform.yml
terraform -chdir=terraform test
terraform -chdir=terraform/bootstrap test
...
python -m unittest discover -s terraform/tests -p 'test_*.py'

This project's CI validate job runs terraform test for both root modules, and separately runs a Python unit test suite (test_initialize_database.py) against terraform/scripts/initialize_database.py — two different test frameworks, for two different languages, in the same job.

Common errors and how to fix them

What you seeLikely causeHow to fix it
“Error: Test assertion failed”An assert condition evaluated to false — the actual computed value doesn't match what the test expects.Read the error_message and the reported actual value; either fix the configuration, or update the test if the expected behavior genuinely changed on purpose.
“No value for required variable” inside a testThe test's own variables block doesn't supply every variable the configuration requires.Add the missing variable to the variables block in that test file (or the run block, which can override it per-run).
A test tries to reach real AzureA run block's provider wasn't actually overridden by mock_provider (for example, a missing providers mapping on the run block in a more complex test file).Confirm the test file's mock_provider block is defined and wired up before any run block that uses command = apply.
“Could not load plugin”Same root cause as every other command: terraform init hasn't run in this folder yet.Run terraform init -backend=false first.
Codey giving a thumbs up

That's all eight commands — from a first init to a fully automated test suite, entirely offline.