Skip to content
advanced Phase 14 · Testing Terraform: Native Tests and Terratest

Mocking Providers in Tests

Replacing provider responses with mock data so tests never touch real clouds.

55m
0 problems
Topic Progress 0%

Running a Full Test Suite with Zero Cloud Credentials

You want every pull request to prove that the module still wires itself correctly — variables flow, conditions pick the right branch, outputs reference the right resources — without handing every developer AWS credentials and without paying for a plan against a real account. The mock_provider block makes that possible inside the native test framework you met in The Native terraform test Framework.

# tests/wiring.tftest.hcl
mock_provider "aws" {
  mock_data "aws_caller_identity" {
    defaults = {
      account_id = "123456789012"
      arn        = "arn:aws:iam::123456789012:root"
    }
  }

  mock_resource "aws_s3_bucket" {
    defaults = {
      arn    = "arn:aws:s3:::mock-bucket"
      region = "us-east-1"
    }
  }
}

run "plan_without_credentials" {
  command = plan
}

Run it with terraform test. The mock provider intercepts every call to the aws provider type: data sources return the fabricated values you listed, and resources get plausible computed values for everything else — Terraform generates random strings for identifiers like id and arn unless you pin them, and derives values it can compute from the configuration itself. No AWS_ACCESS_KEY_ID in the environment, no network round-trips, no per-run cost. A CI job that runs this needs only a Terraform binary.

What the mocked plan still exercises is the part unit tests are for: the module's own logic. The resource graph is built, count and for_each expressions evaluate, locals compute, variable validation runs, and every reference between resources resolves. If someone changes a local that feeds the bucket name, the mocked plan reflects it. If someone deletes the output the downstream module depends on, the test fails. That is exactly the class of bug that used to require a real terraform plan against a sandbox account to catch.

The mechanics mirror real provider resolution. A mock_provider "aws" block acts as the default provider for every aws_-prefixed resource and data source in the configuration under test, so a module that declares no provider block needs no changes at all. Modules called by the root inherit the same standing-in, which is what makes whole-module test runs possible:

run "module_level_plan" {
  command = plan

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

  variables {
    environment = "prod"
  }
}

The module argument points the run at a child module directly — you test the module's interface rather than the example that wraps it, which is the right granularity for a shared module whose callers you do not control.

Version support is the one operational prerequisite: mock_provider arrived in Terraform 1.7, and override_resource and the rest of the family in the same release, so a 1.6 CI image rejects the test file with an "Unsupported block type" error that has nothing to do with your configuration. Pin the test image accordingly.

Everything a mock generates stays schema-shaped: string attributes get plausible random strings, lists get empty lists, booleans get values, and every generated attribute is marked known so downstream expressions and assertions can evaluate. That known-ness is deliberate — it is what lets a mocked plan finish the whole graph where a real plan would stop at (known after apply).

One boundary to internalize before writing assertions: a mock verifies wiring, not the cloud. It has the provider's schema, so a typo in an attribute name still errors, but it enforces none of AWS's rules — bucket naming, policy syntax, AMI existence, IAM permissions. A mocked test passes for configuration that a real apply would reject. Keeping that line sharp is what lets you trust the fast suite and schedule the slow one, and the trade-off gets a full treatment in the last chapter of this topic.

override_resource and override_data: Pinning What Assertions Read

Under a bare mock, computed attributes Terraform generates are random per run. That is fine for an id nobody reads, and disastrous for an assertion:

# flaky: the mocked arn is a random string that changes every run
assert {
  condition     = aws_s3_bucket.logs.arn == "arn:aws:s3:::acme-logs-prod"
  error_message = "bucket ARN does not follow the naming standard"
}

There are two sanctioned ways to make such values deterministic, and they differ in scope.

Defaults inside the mock block apply everywhere the mock is used in the test file. You saw them on mock_data and mock_resource in the previous chapter; they are the right tool when every run wants the same fabricated provider output — a fixed account ID, a fixed region.

Override blocks inside a run block target one resource or data source for that run only, and accept partial attribute sets:

run "policy_references_log_bucket" {
  command = plan

  override_data {
    target = data.aws_caller_identity.current
    values = { account_id = "123456789012" }
  }

  override_resource {
    target = aws_s3_bucket.logs
    values = { arn = "arn:aws:s3:::acme-logs-prod" }
  }

  assert {
    condition     = strcontains(aws_s3_bucket_policy.logs.policy, "arn:aws:s3:::acme-logs-prod/*")
    error_message = "bucket policy must scope object access to the log bucket"
  }
}

Now the assertion reads a value the test itself declared, so the test fails only when the policy wiring changes, never because a random generator rolled differently. Note the constraint that trips people: override_resource's target must be a whole resource or data source address, not an individual instance of a count or for_each resource, and every key in values must be a real attribute of that type — a misspelled attribute is an immediate error, which is the schema check you get for free.

Overrides also solve the data-source trap. Reading an account ID through data.aws_caller_identity normally needs credentials; overridden, the data source returns your fabricated account during the plan and the module's real lookup code still runs against it. That combination — real module code, fabricated provider answers — is the definition of a stub: the test controls the boundary, the configuration under test does not know.

Discipline keeps mocks honest:

Practice Why
Pin only what assertions or downstream wiring read Everything else can stay random and catch more drift
Keep one run per scenario Overrides are per-run; reusing one block hides scenario differences
Prefer asserting config-derived values bucket = "acme-${var.env}" is logic; a generated ARN is noise
Never mock what you do not assert on Mocks that exist "just in case" rot into fiction

The finished pattern — plan-only, mocked, pinned, asserted — is what belongs in the pull-request gate. It runs in seconds, needs no secrets, and answers one question honestly: did the configuration still compile itself into the infrastructure we described? The next chapter turns those asserts into a systematic vocabulary, and Plan Assertions and Expectations extends the same run blocks with expect_failures for guardrail tests.

Precedence is the last mechanical detail: when a run-level override_resource and a file-level mock_resource default both set the same attribute, the run's override wins — per-scenario data beats ambient context. That gives a clean layering: stable world facts (account ID, region) live in the mock block, scenario-specific values (the ARN an assertion names) live in the run. And when a values map contains an attribute the provider schema does not have, Terraform fails immediately with a message naming the real attributes, so typos in tests are caught by the same schema validation that protects configuration.

What Mocks Cannot See: Fidelity Limits and the Integration Fallback

A mocked suite goes green while the real apply dies, and the first time should sting — that pain is the lesson. Walk through what a mock is structurally incapable of detecting, and the test pyramid writes itself.

Provider API rules are invisible. The mock knows the schema — attribute names and types — but none of AWS's acceptance rules. These all pass a mocked plan and fail a real apply:

aws_s3_bucket_name "logs": bucket "Acme_Logs" is invalid;
  bucket names must not contain uppercase letters
aws_iam_role_policy "app": MalformedPolicyDocument: invalid JSON
  in the assume-role policy
aws_instance "web": InvalidAMIID.NotFound: ami-0deadbeef is gone

A mocked suite also cannot notice that an AMI was deregistered last week, that an instance type is unavailable in the chosen zone, or that the execution role lacks s3:CreateBucket. Those are cloud facts, and mocks by design never talk to the cloud.

Apply-time behavior does not exist in plan-only runs. Even with a real provider, command = plan never calls Create, so hangs, eventual-consistency failures, and quota errors stay hidden. Mocking compounds this: your command = apply run against a mock_provider fabricates the post-create state too, so it validates your assertions, not reality.

Random generated values can mask regressions. Because unmocked computed attributes are random per run, an assertion you meant to write against a pinned value may pass against noise for weeks. Audit occasionally: every assert should read either a configuration-derived value or something the test pinned explicitly.

So where does that leave the real cloud? Not abandoned — scheduled. The standard layering:

Layer Runs on Needs credentials Catches
terraform test with mock_provider every PR no wiring, types, guardrails, policy logic
terraform test with the real provider merge / nightly yes API rules, AMIs, permissions, quotas
Terratest apply-and-destroy suites nightly yes end-to-end behavior, eventual consistency

The middle row surprises people: without any mock_provider block, the very same .tftest.hcl file runs against the real provider, so one file can serve both lanes with different credentials. The bottom row — Go-based tests that apply, probe the live system, and destroy — is Terratest Harness in Go, where retry loops handle the eventual consistency that plan-only runs never see.

In an interview, the strong answer to "how do you test Terraform?" is not "Terratest" or "mocks" — it is the pairing: mocked plan tests as the fast feedback loop, real apply tests as the slow truth check, each owning what the other structurally cannot see. Candidates who name the failure modes above — invalid bucket names, missing AMIs, stale data — demonstrate they have watched a green suite fail in production and built the system that keeps it honest.

One more fidelity detail deserves a name: mocks are frozen against the provider version their schema came from. When AWS adds a required attribute or changes a computed one, a mocked suite can keep passing on the old schema while the real provider rejects the configuration — so the mocked lane's green checkmarks need the scheduled real lane beside them as a drift alarm, not merely a backstop. Teams that learn this the hard way usually discover it as a merge-day surprise: three weeks of green mocked plans, then a nightly run fails on a deprecation the mock never modeled.

Quiz

1. A .tftest.hcl file declares `mock_provider "aws"` and a run block with `command = plan`. What does the run require from the environment?

Question 1 options

2. You assert on a resource attribute that the mock provider generates randomly. What problem does that create and what is the fix?

Question 2 options

3. Which defect is a mocked plan test structurally unable to detect?

Question 3 options

4. What is the correct relationship between mocked tests and real-provider tests in a pipeline?

Question 4 options

Flashcards

Question

What does a mock_provider block do in a terraform test file?

Answer

It replaces the named provider type for the test: data sources return the fabricated values given in mock_data defaults, and resources get generated or pinned computed values via mock_resource. No credentials, no network, no cost — while the module's real graph, expressions, and validations still run.

Question

When do you use mock block defaults versus override_resource / override_data in a run block?

Answer

Defaults on the mock block apply to every run in the file — good for stable context like account_id. override_resource / override_data live in one run block and target one address — good for scenario-specific values an assertion reads. Both exist because unmocked computed attributes are random per run.

Question

What are the constraints on override_resource targets and values?

Answer

The target must be an entire resource or data source address, not a single instance of a count or for_each resource. Every key in values must be a real attribute of that type — a misspelled attribute errors immediately, which doubles as a schema check.

Question

Name three classes of defect that mocked tests cannot detect.

Answer

Provider API rules (invalid bucket names, malformed policy JSON), missing or deregistered resources (dead AMIs, unavailable instance types), and permission or quota failures — all require actual cloud calls. Plan-only runs without mocks also miss apply-time behavior such as eventual consistency.

Question

Where do mocked tests belong in the pipeline relative to real-provider tests?

Answer

Mocked plan tests gate every pull request because they are seconds-fast and credential-less. The same test files without mock blocks run against the real provider on merge or nightly, and Terratest apply-and-destroy suites sit at the slowest layer. Each layer catches what the one above cannot.

Revision Notes

Key Takeaways

  • 1. mock_provider fabricates all provider responses, so terraform test plans without credentials, network access, or cost while the module's real logic still executes
  • 2. Computed attributes are random under a mock — pin them with mock_resource defaults or per-run override blocks, or assert only on configuration-derived values
  • 3. override targets are whole resource/data-source addresses and keys must be real attributes; partial values maps leave the rest generated
  • 4. Mocks validate wiring, never cloud rules — invalid names, dead AMIs, and missing permissions pass a mocked plan and fail a real apply
  • 5. The honest strategy is layered: mocked plan tests per PR, real-provider tests on schedule, Terratest for end-to-end behavior

Interview Tips

  • • "What does a mocked Terraform test actually verify?" — the module's graph and logic: expressions, validations, references, guardrails; state clearly it verifies nothing about the cloud's rules
  • • "Why is my mocked assertion flaky?" — you asserted on an auto-generated computed attribute; explain defaults versus override blocks and the per-run randomness that causes it
  • • "How do you test without credentials in CI?" — mock_provider plus command = plan; add that the same file runs against the real provider on a schedule, showing the layering
  • • "When is a mocked test not enough?" — name concrete misses: stale AMIs, malformed policy documents, IAM permissions, quotas — specifics separate real experience from reciting the pattern

Cheat Sheet

Mocking Providers in Tests

mock_provider "aws" {
  mock_data "aws_caller_identity" { defaults = { account_id = "123456789012" } }
  mock_resource "aws_s3_bucket"  { defaults = { arn = "arn:aws:s3:::mock-bucket" } }
}

run "policy_wiring" {
  command = plan
  override_data  { target = data.aws_caller_identity.current
                   values = { account_id = "123456789012" } }
  override_resource { target = aws_s3_bucket.logs
                     values = { arn = "arn:aws:s3:::acme-logs-prod" } }
  assert {
    condition     = strcontains(aws_s3_bucket_policy.logs.policy, "acme-logs-prod")
    error_message = "policy must target the log bucket"
  }
}

Scope: defaults = every run in the file; overrides = one run block; targets = whole addresses, never single instances.

Generated values: unmocked computed attrs are random per run — assert only on pinned or config-derived values.

What mocks catch / miss:

Catches Misses
wiring, types, locals, validations bucket naming rules, policy JSON validity
guardrail logic, removed outputs dead AMIs, quotas, IAM permissions

Pipeline: mocked plans on every PR → real-provider terraform test on merge/nightly → Terratest for full apply-and-destroy behavior.

Continue Learning