Testing
As you develop Terraform modules, ensuring that they work as expected before deploying them into production is important. Terraform provides the terraform test command to help you test your modules directly. By writing tests, you can verify that your infrastructure behaves correctly and consistently across different environments, avoiding potential issues before they arise.
A great blog on Terraform CI/CD and testing, written by Kevon Mayers and Welly Siauw can be read here.
How terraform test Works
Section titled “How terraform test Works”The terraform test command allows you to run tests on your Terraform modules. These tests are written in dedicated files
with the .tftest.hcl extension, and they live alongside your module’s Terraform configuration.
The test files contain assertions that check the behavior of your infrastructure, such as verifying that specific resources are created or that outputs meet expected values. When you run terraform test,
Terraform applies the module to a temporary environment, runs the tests, and destroys the infrastructure afterward to ensure that your environment remains clean.
Terraform executes run blocks in order, simulating a series of Terraform commands executing directly within the configuration directory. The order of the variables and provider blocks doesn’t matter.
Making a .tftest file
Section titled “Making a .tftest file”Instead of creating a separate directory for tests, you are able to create .tftest.hcl files directly in the module’s root
directory, giving the test scope to the resources you wish to validate.
As described over in the docs a Terraform test file is structured with the following elements:
- One to many
runblocks: These blocks define the logic for executing your module and checking its behavior. - Zero to one
variablesblock: This block allows you to define test-specific input variables. - Zero to many
providerblocks: You can configure providers specific to your test cases.
The run Block
Section titled “The run Block”The run block is the heart of your test configuration. This block tells Terraform what to execute and what assertions to check.
You can have one or multiple run blocks within a .tftest.hcl file, each testing a different scenario or configuration.
variable "bucket_name" { type = string default = "test-bucket-1234"}
resource "aws_s3_bucket" "example" { # S3 buckets are private by default bucket = var.bucket_name}run "valid_s3_bucket" {
command = plan description = "Test creation of an S3 bucket" assert { condition = aws_s3_bucket.example.bucket == "test-bucket-1234" error_message = "The S3 bucket name does not match the expected value." }
}The command parameter controls how the test executes. command = plan validates the configuration without creating any real infrastructure — Terraform generates an execution plan and runs your assertions against the planned values. This is ideal for unit-style tests where you want fast feedback without provisioning resources. command = apply (the default) creates real infrastructure, runs assertions against the actual state, and then destroys everything when the test completes.
You are able to override variables and providers from your module, either globally or locally to each test and also
Read more on the run block configuration here
The variables Block
Section titled “The variables Block”The variables block is used to define inputs for your test configuration. These inputs allow you to control the values passed into the resources or modules you are testing. The variables block is optional but very useful for dynamic testing.
variables { bucket_name = "test-bucket-5678"}Declaring the variable globally in your .tftest will allow set the variables used in all the tests in your module.
You can also declare variables at the test level, allowing for different configurations to be run at test time.
run "valid_s3_bucket" { variables { bucket_name = "a-different-name" } assert { condition = aws_s3_bucket.example.bucket == "test-bucket-1234" error_message = "The S3 bucket name does not match the expected value." }}You can learn more on how to use variables in your tests here.
The provider Block
Section titled “The provider Block”The provider block allows you to configure the provider settings specifically for your tests. This block is optional and
is used when your tests require specific provider configurations that differ from your main configuration.
provider "aws" { region = "us-west-2"}Read more on how to use different providers in your tests here.
Putting It All Together
Section titled “Putting It All Together”Here’s an example that uses all three blocks (run, variables, and provider) to test the creation of an AWS S3 bucket.
When you run terraform test, it will:
- Initialize the environment and apply the module.
- Run the assertions you’ve defined.
- Clean up by destroying the resources after the test completes.
- If the assertions pass, you’ll receive confirmation that the test succeeded.
- If any assertions fail, Terraform will provide a clear error message so you can pinpoint what went wrong.
provider "aws" { region = "us-west-2"}
variables { bucket_name = "test-bucket-1234"}
run "basic_test" { description = "Test creation of an S3 bucket with a specific name"
module "s3_bucket" { source = "../" bucket_name = var.bucket_name }
assert { condition = module.s3_bucket.bucket_name == var.bucket_name error_message = "The S3 bucket name does not match the expected value." }}Continuous Validation with check Blocks
Section titled “Continuous Validation with check Blocks”While terraform test validates your modules before deployment, check blocks (Terraform 1.5+) let you validate your infrastructure continuously as part of every plan and apply. A check block defines an assertion about your infrastructure that Terraform evaluates during normal operations — if the assertion fails, Terraform reports a warning rather than an error, so it does not block your deployment.
How check Blocks Differ from Preconditions and Postconditions
Section titled “How check Blocks Differ from Preconditions and Postconditions”Terraform has several validation mechanisms, and each serves a different purpose:
- Preconditions and postconditions are defined inside
resourceordatablocks usinglifecycle. They produce hard errors that stop the plan or apply if the condition fails. Use them when a resource cannot function correctly without the condition being true. - Input validation rules on
variableblocks reject invalid input values before Terraform evaluates any resources. checkblocks are standalone, top-level blocks that produce warnings instead of errors. They are designed for assertions about the broader state of your infrastructure — things you want to know about but that should not prevent other changes from being applied.
This makes check blocks well suited for monitoring invariants like “the website is returning a 200 status code” or “the certificate expires in more than 30 days.” These conditions might fail for reasons outside of Terraform’s control, and blocking the entire plan would not help resolve them.
Basic Syntax
Section titled “Basic Syntax”A check block contains a data source (scoped to the check) and one or more assert blocks. The data source fetches current information, and the assertions evaluate it:
check "website_health" { data "http" "site" { url = "https://example.com" }
assert { condition = data.http.site.status_code == 200 error_message = "Website returned status ${data.http.site.status_code}." }}The data source inside a check block is scoped to that block — it cannot be referenced elsewhere in your configuration. This keeps the check self-contained and avoids polluting your main data sources with monitoring-only queries.
When to Use check Blocks
Section titled “When to Use check Blocks”check blocks are a good fit when you want to verify something about your running infrastructure without blocking deployments:
- Health checks — Confirm that a deployed service is responding correctly after an apply.
- Certificate expiry — Warn when a TLS certificate is approaching its expiration date.
- DNS resolution — Verify that a DNS record points to the expected address.
- External dependencies — Check that an API endpoint or third-party service your infrastructure relies on is reachable.
Because check blocks run on every plan and apply, they act as a lightweight monitoring layer built into your Terraform workflow. If a check fails, you see a warning in the output and can investigate — but the rest of your infrastructure changes proceed as normal.
Read more about check blocks in the Terraform documentation.
Test Mocking
Section titled “Test Mocking”Terraform 1.7 introduced mocking support for terraform test, letting you run tests without real cloud credentials, infrastructure, or cost. Mocked tests execute in seconds because Terraform never calls a real provider API — it generates placeholder values for every resource and data source instead. This makes mocking ideal for fast feedback loops in CI pipelines and local development.
mock_provider
Section titled “mock_provider”A mock_provider block tells Terraform to replace an entire provider with a fake implementation. Every resource and data source managed by that provider returns synthetic values (empty strings, false, 0, and so on) without making any API calls.
mock_provider "aws" {}
run "bucket_is_planned" { command = plan
assert { condition = aws_s3_bucket.example.bucket == "test-bucket-1234" error_message = "Bucket name does not match the expected value." }}Because no real AWS credentials are needed, this test can run anywhere — a developer laptop, a CI runner, or a sandboxed environment — with zero cloud cost.
override_resource and override_data
Section titled “override_resource and override_data”Sometimes the synthetic defaults from mock_provider are not enough. You may need a resource or data source to return a specific value so that downstream logic can be tested. override_resource and override_data let you pin individual attributes to the values you need.
mock_provider "aws" { override_resource { target = aws_s3_bucket.example values = { arn = "arn:aws:s3:::test-bucket-1234" hosted_zone_id = "Z3AQBSTGFYJSTF" } }
override_data { target = data.aws_caller_identity.current values = { account_id = "123456789012" } }}
run "arn_is_correct" { command = plan
assert { condition = aws_s3_bucket.example.arn == "arn:aws:s3:::test-bucket-1234" error_message = "Bucket ARN does not match the overridden value." }}Overrides are useful when your configuration references computed attributes — such as ARNs, IDs, or account numbers — that would otherwise be empty in a fully mocked run.
Mixing Mocked and Real Providers
Section titled “Mixing Mocked and Real Providers”You can mock some providers while keeping others real by using provider aliases. This is helpful when you want to test against a real backend (for example, a Terraform state store) but mock the provider that provisions expensive or slow resources.
mock_provider "aws" { alias = "fake"}
provider "aws" { region = "us-west-2"}
run "mixed_providers" { providers = { aws = aws # real provider for resources that need it aws.mock = aws.fake # mocked provider for everything else }
assert { condition = aws_s3_bucket.example.bucket == "test-bucket-1234" error_message = "Bucket name does not match." }}The alias approach gives you fine-grained control over which parts of your configuration talk to real APIs and which parts run against mocks.
When to Use Mocking vs Plan-Only Tests
Section titled “When to Use Mocking vs Plan-Only Tests”Both command = plan and mock_provider let you test without creating real infrastructure, but they work differently:
| Approach | Credentials required | Computed values available | Speed |
|---|---|---|---|
command = plan | Yes — Terraform calls the provider to generate the plan | Yes — the provider calculates computed attributes | Fast, but requires valid credentials |
mock_provider | No — the provider is never called | Only synthetic defaults or explicit overrides | Very fast, no credentials needed |
Use command = plan when you need accurate computed values from the provider (such as ARNs or availability-zone lists) and have credentials available. Use mock_provider when you want the fastest possible feedback, need to run tests without any cloud access, or want to eliminate cost entirely.
The two approaches can be combined in the same test file — some run blocks can use mocked providers while others use real ones.
Read more about test mocking in the Terraform documentation.
Conclusion
Section titled “Conclusion”terraform test, check blocks, and test mocking give you three complementary approaches to validating your Terraform code. Tests verify that your modules behave correctly before deployment, check blocks monitor your running infrastructure on every plan and apply, and mocking lets you run tests without cloud credentials, cost, or delays.
Writing tests gives you early error detection and lets you validate changes before they reach a real environment, reducing the risk of unexpected failures during deployment. Adding check blocks extends that validation into your ongoing workflow, surfacing warnings about infrastructure drift or external issues without blocking your changes. Mocking takes this further by removing the need for real provider access, making it practical to run your full test suite in any environment.