Skip to content

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.


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.

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 run blocks: These blocks define the logic for executing your module and checking its behavior.
  • Zero to one variables block: This block allows you to define test-specific input variables.
  • Zero to many provider blocks: You can configure providers specific to your test cases.

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.

main.tf
variable "bucket_name" {
type = string
default = "test-bucket-1234"
}
resource "aws_s3_bucket" "example" {
# S3 buckets are private by default
bucket = var.bucket_name
}
main.tftest.hcl
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 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.

main.tftest.hcl
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 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.

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."
}
}

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 resource or data blocks using lifecycle. 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 variable blocks reject invalid input values before Terraform evaluates any resources.
  • check blocks 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.

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.

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.

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.

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.

main.tftest.hcl
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.

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.

main.tftest.hcl
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.

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.

main.tftest.hcl
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.

Both command = plan and mock_provider let you test without creating real infrastructure, but they work differently:

ApproachCredentials requiredComputed values availableSpeed
command = planYes — Terraform calls the provider to generate the planYes — the provider calculates computed attributesFast, but requires valid credentials
mock_providerNo — the provider is never calledOnly synthetic defaults or explicit overridesVery 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.

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.