terraform test (Terraform 1.6+) runs your module with test inputs and checks the result with assertions. Tests live in *.tftest.hcl files, usually in a tests/ folder next to the module.
flowchart LR TF["tests/app.tftest.hcl"] --> R1["run plan_defaults (command = plan)"] R1 --> R2["run rejects_bad_env (expect_failures)"] R2 --> R3["run apply_real (command = apply)"] R3 --> D["Destroy everything created"] MK["mock_provider docker"] -.-> R1 %% step TF: A test file holds one or more run blocks that execute in order. %% step R1: A plan-only run is fast and creates nothing; assertions check the planned values. %% step R2: expect_failures passes only if the listed validation or precondition fails. %% step R3: An apply run creates real resources, so you can assert on computed values. %% step D: When the file finishes, Terraform destroys what the runs created, in reverse order. %% step MK: A mock provider returns fake values, so even apply runs need no real infrastructure.
A test file
# modules/app/tests/app.tftest.hcl
variables {
name = "shop-api"
environment = "dev"
}
run "names_follow_convention" {
command = plan
assert {
condition = docker_container.this.name == "shop-api-dev"
error_message = "container name must be <name>-<environment>"
}
}
run "rejects_unknown_environment" {
command = plan
variables {
environment = "qa"
}
expect_failures = [var.environment]
}
run "prod_needs_two_replicas" {
command = plan
variables {
environment = "prod"
replicas = 1
}
expect_failures = [docker_container.this] # its precondition must fail
}
cd modules/app
terraform init
terraform test # all files in . and tests/
terraform test -filter=tests/app.tftest.hcl -verbose
Unit tests with mocks
mock_provider "docker" {
mock_resource "docker_container" {
defaults = { id = "mock-1234", exit_code = null }
}
}
With a mock provider, command = apply returns made-up computed values instead of calling Docker or a cloud API: fast and free. Use override_resource or override_data inside a run to fix one value.
What to test
| Test type | How | Speed |
|---|---|---|
| Input validation | command = plan + expect_failures |
Seconds |
| Naming, counts, conditionals | command = plan + assert |
Seconds |
| Logic with computed values | Mock provider + command = apply |
Seconds |
| Real integration | command = apply against LocalStack or a sandbox account |
Minutes, costs money in a real cloud |
Each run can also use a different module with module { source = "./tests/setup" } to create prerequisites first.
Traps: an apply run that fails half-way can leave resources behind if the destroy also fails, so run integration tests in a throwaway account or LocalStack; and assertions on values that are (known after apply) fail in a command = plan run, so switch that run to apply (or mock it).