Terraform's Native Test Framework: terraform test, Mocking, and When You Still Need Terratest

Quick answer
terraform test has been built into the CLI since 1.6, and since 1.7 it can mock providers entirely — asserting on plan output without touching a real cloud account. Here's how run blocks, assert blocks, and mock_provider actually work, and where the native framework stops and Terratest still has to take over.
- Test Files and the Basic Structure
- Mock Providers: Testing Plan Logic Without a Cloud Account
- Native terraform test vs. Terratest
7 min read · DevOps & Platform
Before Terraform 1.6, testing a module meant either terraform plan and eyeballing the diff, or reaching for Terratest — a Go library that actually provisions real infrastructure, asserts against it, and tears it down. Terratest works, but it means a Go toolchain, real cloud credentials in CI, and real money spent on every test run.
terraform test, built into the CLI since 1.6 (October 2023), closes part of that gap: HCL test files, no separate language, no Go required. Since 1.7, it can also mock providers entirely — asserting on plan logic without ever calling a cloud API. That's the part worth understanding, because it changes what a "fast, free, deterministic" Terraform test actually looks like.
Test Files and the Basic Structure
Terraform discovers test files by extension (.tftest.hcl or .tftest.json) — it doesn't require a specific directory. The convention nearly everyone follows anyway is a tests/ subdirectory alongside the module, since it keeps test files out of the way of main.tf/variables.tf:
modules/vpc/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
└── vpc_naming.tftest.hcl
A test file is a sequence of run blocks, each executing either a plan or an apply against the configuration:
1# tests/vpc_naming.tftest.hcl
2variables {
3 environment = "staging"
4 vpc_cidr = "10.0.0.0/16"
5}
6
7run "plan_produces_expected_tags" {
8 command = plan
9
10 assert {
11 condition = aws_vpc.this.tags["Environment"] == "staging"
12 error_message = "VPC tags[Environment] did not match the environment variable"
13 }
14
15 assert {
16 condition = can(regex("^vpc-", output.vpc_name))
17 error_message = "vpc_name output must be prefixed with 'vpc-'"
18 }
19}
20
21run "apply_creates_vpc_with_correct_cidr" {
22 command = apply
23
24 assert {
25 condition = aws_vpc.this.cidr_block == var.vpc_cidr
26 error_message = "VPC CIDR block did not match the input variable"
27 }
28}Run it with terraform test from the module directory. command = plan only runs a plan — fast, and it catches logic errors (wrong tag, wrong naming convention, wrong conditional) without provisioning anything. command = apply actually creates real resources against whatever provider and credentials are configured, then destroys them at the end of the file — this is the Terratest-equivalent mode, and it costs real time and money the same way.
Each run block can override input variables for that specific run, so one test file can exercise a module across multiple input combinations without duplicating the configuration:
1run "rejects_invalid_environment" {
2 command = plan
3 variables {
4 environment = "prod-typo"
5 }
6 expect_failures = [var.environment]
7}expect_failures asserts that a variable's validation block (or a resource precondition) actually fails for bad input — useful for testing the guardrails you wrote, not just the happy path.
Mock Providers: Testing Plan Logic Without a Cloud Account
This is the headline feature, and it's what actually makes terraform test a viable unit-testing tool rather than just a thinner Terratest. mock_provider replaces a real provider with one that returns schema-valid, auto-generated fake values — no credentials, no network calls, no cloud account required:
1mock_provider "aws" {}
2
3run "naming_convention_is_correct" {
4 command = plan
5
6 assert {
7 condition = can(regex("^payments-prod-", aws_s3_bucket.this.bucket))
8 error_message = "Bucket name must be prefixed with '<app>-<env>-'"
9 }
10}The catch: mocked computed attributes (ARNs, generated IDs, IP addresses) are randomly generated on every run by default, since the mock provider doesn't know what a real API would return. If your assertion depends on a specific computed value rather than just its shape, pin it with override_resource or override_data:
1mock_provider "aws" {
2 override_resource {
3 target = aws_kms_key.this
4 values = {
5 arn = "arn:aws:kms:us-east-1:123456789012:key/mock-key-id"
6 }
7 }
8}
9
10run "output_references_correct_kms_arn" {
11 command = plan
12
13 assert {
14 condition = output.encryption_key_arn == "arn:aws:kms:us-east-1:123456789012:key/mock-key-id"
15 error_message = "encryption_key_arn output did not pass through the KMS key ARN"
16 }
17}override_data does the same thing for data sources — useful for mocking an aws_caller_identity or aws_availability_zones lookup so a test doesn't depend on which account or region actually runs it.
Terraform Day-2 Operations Checklist
State hygiene, drift, imports, policy checks, and upgrade routines — everything after `terraform apply` works. Plain Markdown, commit it to your repo.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
Native terraform test vs. Terratest
They solve different problems, and most teams end up using both rather than picking one:
Native terraform test | Terratest | |
|---|---|---|
| Language | HCL — no new syntax to learn | Go |
| Setup cost | Built into the terraform binary | Go toolchain, module dependencies |
| Mocked runs | Yes, via mock_provider — free, instant, no cloud account | No — always provisions real infrastructure |
| Real infrastructure validation | Yes, via command = apply, but no built-in helpers beyond Terraform itself | Yes, plus a large ecosystem of helper functions (HTTP checks, SSH into instances, Kubernetes client, AWS SDK assertions) |
| Best fit | Fast unit-style tests on plan logic: naming, tagging, conditionals, variable validation | True integration tests: does this module actually produce a reachable, correctly-configured resource in a real account |
Use native tests for the things that would otherwise only surface in code review — a typo'd tag key, a conditional that doesn't branch the way you think, a variable validation rule with a logic bug — and run them on every PR, since mocked tests cost nothing. Reserve Terratest (or command = apply native tests) for the smaller set of modules where you genuinely need to prove the deployed resource behaves correctly, and accept that those tests are slower and cost money to run.
Frequently Asked Questions
Does terraform test replace terraform plan in CI?
No — they check different things. terraform plan in a PR shows a human what will change in a specific environment's real state. terraform test runs against ephemeral, often-mocked state to verify the module's logic is correct regardless of environment. Most CI pipelines run both: terraform test as a fast pre-merge gate, terraform plan against the target environment before an actual apply.
Can I test for a plan that should fail entirely, not just a specific variable?
Yes. Omit expect_failures and instead assert on the error using a resource or variable precondition/postcondition block in the module itself, or check run.<name>.state behavior in a later run. For simple "this variable's validation should reject this input" cases, expect_failures = [var.name] is the direct tool.
Do mocked tests count as real coverage, or are they just syntax checking?
Somewhere in between. A mocked terraform test run does execute your actual count/for_each logic, conditionals, and variable interpolation — a real bug in that logic will fail the assertion. What it can't catch is anything that depends on the real provider's actual behavior: an AWS API rejecting a combination of parameters your HCL considers valid, a race condition between two resources, or IAM permission errors. Mocked tests are a strong first filter, not a replacement for periodic apply-mode or Terratest runs against real infrastructure.
Does this work with modules that use for_each over a data source?
Yes, including with mocked data sources via override_data — mock the data source's return value to a fixed map, and for_each iterates over that mocked value exactly as it would over a real one. This is one of the more useful patterns for testing multi-resource modules without needing a real account to populate the data source in the first place.
For the broader IaC testing and policy landscape this fits into, see OPA, Sentinel, and Checkov for Terraform Policy as Code. For the EKS infrastructure this kind of test suite is usually protecting, see Terraform for Kubernetes: Managing EKS with Infrastructure as Code.
Building out a Terraform testing strategy for a team that's been skipping it? Talk to us at Coding Protocols — we help platform teams add CI-gated tests to existing infrastructure code without a rewrite.
Official References
- Terraform tests (HashiCorp) — run blocks, assert blocks, variables, expect_failures
- Provider mocking in tests — mock_provider, override_resource, override_data
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


