TerraformXXI · Testing, Linting, and Static AnalysisProduction Terraform
Mock Providers for Unit Tests
What you'll learn
- Use the `mock_provider` and `mock_data` blocks in a `.tftest.hcl` file
- Define override_resource and override_data attributes that drive the test
- Choose the right boundary for what to mock and what to leave real
- Recognise the cost of over-mocking: a green test that does not prove anything
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
A unit test in terraform test runs without the real
provider. The provider is replaced by a stub that the test
author writes inline. The stub has two halves: a
mock_provider block that replaces the provider plugin, and
a mock_data block that replaces a specific data source. The
two halves answer different questions. Knowing which one to
use, and at what boundary, is the difference between a test
that catches regressions and a test that gives false
confidence.
What the mock is
The mock_provider block is a provider-block-shaped stub.
When the test passes it to terraform test, the engine
treats it as the real provider for the duration of the test.
The block declares the provider’s empty behaviour; the test
adds the overrides.
# modules/s3_bucket/tests/unit.tftest.hcl
mock_provider "aws" {
alias = "mock"
}
run "creates_bucket" {
command = plan
assert {
condition = aws_s3_bucket.this.bucket == "test-bucket"
error_message = "bucket name did not propagate"
}
}
The alias = "mock" is optional. If the resource under test
uses an aliased provider, the mock must match the alias.
What the mock is not
The mock provider is not a schema validator. It accepts any attribute. A test that passes against the mock does not prove the real provider would accept the configuration. The unit test catches configuration logic; the integration test catches provider schema drift. The two are not redundant.
Real provider: schema-checked, attribute-validated, region-aware
Mock provider: schema-blind, accepts anything, no region constraint
mock_provider versus mock_data
The two blocks answer different questions and they are not interchangeable.
mock_provider replaces the provider plugin. It
applies to every resource and data source from that
provider. You use it when the test does not need to talk
to the cloud at all.
mock_data replaces a specific data source. You use
it when the configuration reads from a data source that
the test cannot easily provide. The override applies to
that data source only; the rest of the provider is real
or mocked separately.
# modules/vpc/tests/unit.tftest.hcl
# Mock a single data source that depends on the AWS API
mock_data "aws_availability_zones" {
defaults = {
names = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}
}
run "spans_three_zones" {
command = plan
assert {
condition = length(aws_subnet.public) == 3
error_message = "expected one subnet per AZ"
}
}
The defaults block provides the values the data source
would normally return. The rest of the configuration reads
the provider the way it normally would. The test is
deliberately narrower than a mock_provider test: only
the data source is fake.
Override semantics
The override rules for mock_provider follow the same
resource hierarchy as the real provider:
mock_provider "aws" {
alias = "mock"
# Every aws_instance in the test returns this value
override_resource {
target = aws_instance.this
values = {
id = "i-1234567890abcdef0"
arn = "arn:aws:ec2:eu-west-1:123456789012:instance/i-1234567890abcdef0"
tags = { Name = "mocked" }
}
}
# Every aws_data_block returns this value
override_data {
target = data.aws_caller_identity.current
values = {
account_id = "123456789012"
arn = "arn:aws:iam::123456789012:user/example"
}
}
}
The target field is the address of the resource or data
source. The values field is a map of attribute names to
the values the test wants to return. Anything not in the
map is left as null (or the default). The test then
asserts on the values.
run "instance_has_expected_arn" {
command = plan
assert {
condition = aws_instance.this.arn == "arn:aws:ec2:eu-west-1:123456789012:instance/i-1234567890abcdef0"
error_message = "ARN did not match the override"
}
}
The right boundary
The productive question is: what does the test need to control, and what does the test need to prove?
Need to control (mock):
- The data source that depends on the cloud API
- A resource attribute that the test asserts on
Need to prove (leave real):
- The configuration logic
- The conditional branches
- The output values
- The resource graph
A unit test that mocks the resource it asserts on is proving the mock works. A unit test that mocks the data source and asserts on the resource is proving the configuration logic. The second test is the one you want.
When not to use mocks
The mock is the wrong tool when:
- The test depends on the provider schema. A mock happily accepts an attribute that the real provider rejects. The test is wrong by construction.
- The test depends on the order of provider API calls. The mock makes no calls.
- The test depends on the real provider’s response semantics (eventual consistency, retries). The mock has no such semantics.
- The change is a provider upgrade. The whole point of the test is to verify the upgrade. The mock would pass against the old schema anyway.
In all four cases, the right answer is an integration test against a sandbox. The unit test is not the gate.
Production failure modes
1. The test mocks the resource it asserts on
Symptom: the test passes locally, fails in the next provider bump. Cause: the override is the only thing the test asserts on. The test is a tautology. Fix: mock the data source, not the resource. The unit test should exercise the configuration, not the mock.
2. The mock accepts a deprecated attribute
Symptom: terraform test passes, but the integration
suite fails on a provider upgrade. Cause: the mock accepts
attributes the real provider has removed. Fix: add an
integration test for the module. Provider-schema drift
is the integration test’s job, not the unit test’s.
3. The test is slower than the integration test
Symptom: terraform test takes 30 seconds because the
module has 40 mocked resources and the engine runs them
all. Cause: the mocks are too granular. The test is
modelling the cloud, not the configuration. Fix: reduce
the number of mock overrides. Mock the data source; let
the resource graph fall out of the configuration.
4. The override values are stale
Symptom: an assertion fails on arn == "arn:aws:ec2:..."
because the contributor changed the region. Cause: the
override values are hand-written. They are not the
authority. Fix: build overrides from a helper that takes
the region and account ID as variables. The contributor
who changes the region changes the helper, not 40
overrides.
5. The mock is shared across incompatible tests
Symptom: a tftest.hcl file uses an override for
aws_instance; another test in the same module uses a
different override. The first test fails because the
mock_provider block is reused. Cause: the
mock_provider block is global to the file. Fix: split
the tests into separate tftest.hcl files. Each file
has its own mock.
6. The test passes locally but fails in CI
Symptom: the same test exits 0 on the developer’s machine, exits 1 in CI. Cause: the mock provider needs the same Terraform version as the configuration. A version mismatch silently uses the wrong schema. Fix: pin the Terraform version in CI. The mock is version-aware in the same way the real provider is.
Security and performance implications
Mock providers are local-only. They make no network calls. They produce no audit log. The test runs in process.
Performance: a unit test with one mock_provider block
and a handful of overrides runs in under a second. A test
that mocks every resource in a 40-resource module runs in
seconds. The cost scales with the number of overrides, not
the size of the configuration.
Security: the mock provider does not validate credentials. The unit test does not need AWS access. The CI does not need a role. The unit test is the cheapest gate in the pipeline and the safest to run on a developer laptop.
Production guidance
- Mock the data source. Leave the resource graph real.
- Use
override_resourceandoverride_dataas the exception, not the rule. The default is “no override.” - Run the unit test against the same Terraform version as the rest of the pipeline.
- Build override values from a helper. Hand-written override strings rot.
- Add an integration test for any module that mocks heavily. The cost of the integration test is the receipt for the unit test’s blind spots.
- Do not mock the resource under test. The test is proving the configuration, not the mock.
Verification
# Run the unit tests
terraform test -test-directory=tests/unit
# List the test files
ls -la tests/**/*.tftest.hcl
# Show what the mock is asserting
terraform test -verbose
A clean unit test exits 0 and produces no diagnostics. A
failed assertion exits non-zero and prints the file and
line of the failing assert block.
Knowledge check · 7 questions
Q1. What does `mock_provider` do in a terraform test file?
Q2. The mock provider accepts any resource attribute without checking it against the provider schema.
Q3. When should you use `mock_data` instead of `mock_provider`?
Q4. What is the cost of over-mocking a unit test?
Q5. Which of the following are good rules for mocking in terraform test? (Select all that apply.)
Q6. A unit test against `mock_provider` passes, but the integration test fails on a provider upgrade. What is the most likely cause?
Q7. A team writes a unit test that mocks the `aws_instance` resource and asserts on the override value. The test passes. Production breaks. What is the next step?
Passing score: 75%. Answers are checked in this browser.