Skip to main content
RunBook Academy

TerraformXXI · Testing, Linting, and Static AnalysisProduction Terraform

Mock Providers for Unit Tests

Advanced⏱ ~14 minbash

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

Not yet marked complete on this device.

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_resource and override_data as 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

  1. Q1. What does `mock_provider` do in a terraform test file?

  2. Q2. The mock provider accepts any resource attribute without checking it against the provider schema.

  3. Q3. When should you use `mock_data` instead of `mock_provider`?

  4. Q4. What is the cost of over-mocking a unit test?

  5. Q5. Which of the following are good rules for mocking in terraform test? (Select all that apply.)

  6. Q6. A unit test against `mock_provider` passes, but the integration test fails on a provider upgrade. What is the most likely cause?

  7. 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.