Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 10 min read

Mocking Providers in Terraform Tests for Fast, Offline Runs

Use mock_provider and override_resource/override_data/override_module in terraform test to write fast offline unit tests, with AI scaffolds reviewed by humans.

  • #terraform
  • #testing
  • #mock-provider
  • #ci
  • #ai

I used to dread running my Terraform module tests. Every terraform test invocation spun up real VPCs, real S3 buckets, and real IAM roles, then tore them down again. A single suite took twelve minutes, leaked the occasional orphaned resource, and once cost me a surprise NAT gateway bill because a teardown failed mid-run. The feedback loop was so slow that I stopped running tests locally and just pushed to CI and prayed.

Then Terraform 1.7 shipped mock_provider and the override_* blocks, and the math changed completely. I can now run a meaningful chunk of my test suite offline, in under two seconds, with zero cloud credentials in scope. This post is how I do it, where the mocks lie to you, and how I use AI to write the scaffolds without letting it fake my coverage.

Why mock a provider at all

The terraform test framework runs run blocks that execute a plan or apply against your configuration. With real providers, apply actually creates infrastructure. That’s integration testing, and it’s valuable, but it’s slow and it requires credentials.

A lot of what I want to test isn’t “does AWS create this bucket correctly” — AWS’s provider already tests that. What I want to test is my logic: did the for_each produce the right number of resources, did the naming convention render correctly, did the conditional count flip when I set a variable, does an invalid input get rejected. None of that needs a real cloud. It needs the plan graph to resolve.

That’s exactly what mock_provider gives you: a fake provider that returns synthetic values for every resource and data source so plans and applies resolve without any API calls.

Pro Tip: Mock the providers for unit tests that assert on your module’s logic, and keep a small set of real-provider integration tests behind a separate, slower CI job. Don’t try to mock your way to confidence that infrastructure actually provisions.

A minimal mocked test file

Test files end in .tftest.hcl and live alongside your module or in a tests/ directory. Here is the smallest useful one. It declares a mock AWS provider and runs a plan.

# tests/naming.tftest.hcl

mock_provider "aws" {}

run "bucket_name_is_prefixed" {
  command = plan

  variables {
    environment = "staging"
    project     = "billing"
  }

  assert {
    condition     = aws_s3_bucket.this.bucket == "billing-staging-data"
    error_message = "Bucket name did not match the expected naming convention"
  }
}

With the empty mock_provider "aws" {} block, every AWS resource and data source gets auto-generated mock values. Computed attributes that Terraform would normally mark as “known after apply” get deterministic fake values instead, so command = plan can resolve and your assert conditions can read them.

Running it

The CLI is just terraform test. It discovers every .tftest.hcl file, runs each run block in order, and reports pass/fail per assertion:

$ terraform test
tests/naming.tftest.hcl... in progress
  run "bucket_name_is_prefixed"... pass
tests/naming.tftest.hcl... teardown
tests/naming.tftest.hcl... pass

Success! 1 passed, 0 failed.

Because nothing real was created, the teardown phase is instant — there’s nothing to destroy. You can point at a single file with terraform test -filter=tests/naming.tftest.hcl while iterating.

Overriding specific resources and data sources

Empty mocks are fine until your module reads a data source whose value drives a decision. If your module looks up an AMI or an availability-zone list, the auto-generated mock value is arbitrary, and your assertions can’t depend on it. That’s where override_data, override_resource, and override_module come in — they let you pin exact values.

# tests/azs.tftest.hcl

mock_provider "aws" {}

run "subnets_span_three_azs" {
  command = plan

  # Pin the data source the module reads.
  override_data {
    target = data.aws_availability_zones.available
    values = {
      names = ["us-east-1a", "us-east-1b", "us-east-1c"]
    }
  }

  variables {
    cidr_block = "10.0.0.0/16"
  }

  assert {
    condition     = length(aws_subnet.private) == 3
    error_message = "Expected one private subnet per availability zone"
  }
}

override_resource works the same way for managed resources when you need a specific computed attribute — say a generated ARN — to be stable so a downstream interpolation is predictable:

  override_resource {
    target = aws_kms_key.this
    values = {
      arn = "arn:aws:kms:us-east-1:111122223333:key/test-key"
    }
  }

And override_module replaces an entire child module’s outputs without planning its internals — handy when a submodule itself talks to a provider you don’t want to exercise:

  override_module {
    target = module.vpc
    outputs = {
      vpc_id     = "vpc-0abc123"
      subnet_ids = ["subnet-aaa", "subnet-bbb", "subnet-ccc"]
    }
  }

You can place any of these override_* blocks inside a run block to scope them to that test, or at the top level of the file to apply them to every run.

Setting defaults with mock_resource

When most of your tests want the same fake shape for a resource type, define it once on the provider with mock_resource (and mock_data for data sources) instead of repeating override_resource everywhere:

# tests/defaults.tftest.hcl

mock_provider "aws" {
  mock_resource "aws_s3_bucket" {
    defaults = {
      arn = "arn:aws:s3:::mocked-default-bucket"
    }
  }

  mock_data "aws_caller_identity" {
    defaults = {
      account_id = "111122223333"
    }
  }
}

run "tagging_uses_account_id" {
  command = plan

  assert {
    condition     = aws_s3_bucket.this.tags["Owner"] == "111122223333"
    error_message = "Owner tag should be the caller account id"
  }
}

defaults set baseline values for every instance of that type, and a per-run override_resource still wins if you need to specialize one case.

Where I let AI help, and where I don’t

Writing these files is repetitive: same provider block, same run skeleton, same assertion shape, dozens of times across a module library. That’s exactly the kind of boilerplate an LLM is good at. I treat the model as a fast junior engineer — it drafts the scaffold, I own the judgment.

A prompt I lean on: “Here is my module’s variables.tf and main.tf. Generate a .tftest.hcl that mocks the AWS provider, overrides data.aws_availability_zones.available, and asserts the subnet count equals the number of AZs.” Whether I’m working in Claude, Cursor, or GitHub Copilot, the output is a useful first draft — and a draft is all it is. Reusable versions of these prompts live in my prompt library and the deeper prompt packs.

There are hard rules I never bend:

  • AI never runs apply against real infrastructure. The model writes tests; it doesn’t execute them with cloud creds. No state-write access, no provider credentials in the agent’s environment. Mocked runs are safe precisely because there’s nothing to break, but the moment a real provider is in play, a human drives.
  • I review every plan. I read what the generated test actually asserts before I trust a green check.
  • I check that tests aren’t tautological. This is the big one with mocks.

Pro Tip: The fastest way to expose a fake test is to break the code it claims to cover. Flip a + to a - in your naming logic; if the suite stays green, the assertion was asserting on a mock value the module never computed, not on your logic.

How mocks let coverage get faked

The trap with override_* is that you can pin a value and then assert on that same value, which tests nothing. If you write override_resource setting arn = "arn:test" and then assert that the arn equals "arn:test", you’ve tested that Terraform can echo a constant. AI loves to write these because they always pass, and a passing test looks like done work.

So when I review a generated test I ask: does this assertion read something the module computed, or something the test injected? A real assertion checks the count of resources my for_each produced, the string my format() rendered, the structure my dynamic block built — values that depend on my code and my variables, not on the override. Overrides should feed inputs; assertions should check outputs of your logic. If those two are the same value, delete the test. I run generated test files through the same code review pass I use for application changes, looking specifically for that input-equals-output smell.

Conclusion

mock_provider and the override_* blocks turned my Terraform tests from a twelve-minute integration ritual into a sub-second unit-test loop I actually run before every commit. Mock the provider for logic, keep a thin real-provider suite for provisioning, and let AI draft the scaffolds. Just remember that a mocked test only proves what your assertions genuinely check — so review every one, break your own code to confirm the tests bite, and never hand the model the keys to a real cloud. Start with the terraform guides if you want more in this vein.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.