Why mocks
terraform test with command = apply calls the provider by default. AWS
costs money and adds latency; LocalStack has to be started and waited on.
For unit-testing a module that is overkill: you can verify that a bucket
name generates the right ARN without a single API call.
A mock provider answers every provider call with synthetic data. Available since Terraform 1.7.
mock_provider for the whole file
# tests/unit.tftest.hcl
mock_provider "aws" {}run "naming" {command = apply # apply is fine, mock does not reach the cloud
variables {name = "test-bucket"
}
assert {condition = aws_s3_bucket.this.bucket == "test-bucket"
error_message = "name not propagated"
}
}
mock_provider "aws" {} with no arguments gives every AWS resource and
data source default mock values. Terraform generates placeholders
automatically: strings get "unknown", numbers get 0, booleans get
false, sets and lists get empty collections.
Apply "runs", but nothing reaches the cloud.
Default values for specific types
If the generated defaults break your logic (for example, a module reads
aws_s3_bucket.this.id and builds other names from it), set explicit
defaults:
mock_provider "aws" { mock_resource "aws_s3_bucket" { defaults = {id = "mocked-bucket-id"
arn = "arn:aws:s3:::mocked-bucket"
}
}
mock_data "aws_caller_identity" { defaults = {account_id = "111111111111"
arn = "arn:aws:iam::111111111111:user/test"
user_id = "AIDA1234567890"
}
}
}
Now aws_s3_bucket.this.id will consistently return mocked-bucket-id
for every test in that file.
Per-run override
File-level mocks are identical across all run blocks. When one scenario
needs a specific value, use override_resource or override_data inside
that run:
run "private_bucket_acl" {command = apply
override_resource {target = aws_s3_bucket.this
values = {id = "specific-private-bucket"
arn = "arn:aws:s3:::specific-private-bucket"
}
}
assert {condition = aws_s3_bucket_acl.this.bucket == "specific-private-bucket"
error_message = "ACL refers to wrong bucket"
}
}
target is the resource address inside the module under test. An override
takes precedence over mock_resource ... defaults at the file level.
Override without mock_provider
If most of your tests are real (running against LocalStack) but you want
to stub out one data source to avoid an external dependency, skip
mock_provider entirely and use only an override:
run "with_fixed_account" {command = plan
override_data {target = data.aws_caller_identity.current
values = {account_id = "999999999999"
}
}
assert {condition = local.bucket_name == "logs-999999999999"
error_message = "account_id not woven into name"
}
}
All other resources go to the real provider (or LocalStack, whichever is
configured). Only aws_caller_identity is substituted.
Which tests need mocks, which need real providers
| What you are testing | Test type |
|---|---|
| Name generation from variables | mock, no cloud needed |
Conditional logic (count = var.enabled ? 1 : 0) | mock |
| Value passing between module resources | mock |
| That the module actually creates a resource with the right config | real (LocalStack or AWS) |
| That cross-resource policies (bucket policy) are applied correctly | real |
The rule: if an assertion can be written without attributes that are "known after apply", use a mock. You will save hours in CI.
Mocks do not require init -upgrade
terraform init with a mock provider completes faster because Terraform
does not download the real provider binary. It only fetches the schema
from the official plugin for type-checking. In CI that saves several
minutes.
Default behavior: what a mock returns
When you do not set defaults, the mock provider generates values from
the attribute's schema:
| Attribute type | Mock value |
|---|---|
string | "foo" |
number | 0 |
bool | false |
list/set/map | empty |
object | object with defaults for nested fields |
| Optional, not required | null |
This means attributes like id and arn receive the string "foo". If
two references both read from different aws_s3_bucket.X.id resources,
they both get the same "foo". Any logic that depends on uniqueness
will break. The fix: set defaults for each resource.
Pitfalls
-
Mocks only work inside
*.tftest.hcl. Plain.tffiles have nomock_providerblock. You cannot "test with terraform plan by hand". Useterraform test. -
Attributes computed by complex provider logic are not modeled.
aws_iam_policy_documentis a data source that renders JSON locally. A mock returns a placeholder JSON blob, not a valid policy. To verify IAM policies you need the real data source or an explicit override for every field. -
Mock defaults are not fixtures. They do not persist state across
runblocks. If the first run modifiesaws_s3_bucket.this, the second run gets the mock default again, not the modified value. The test runner likes this behavior; the test author sometimes does not. -
override_resourceuses the module-local address. Writeaws_s3_bucket.this, notmodule.X.aws_s3_bucket.this. The assumption is that the test runs from the module's own directory. -
A provider-level mock does not cover sources declared in a provider with a different alias. For
aws.euyou need a separatemock_provider "aws" { alias = "eu" }.