In today’s blog post, we will learn Terraform test block used for integration or unit testing and validations. Introduced in Terraform v1.5, the test and run blocks allow you to write detailed unit, integration, and even basic end-to-end tests directly in HCL. This removes the need for external frameworks for many common testing scenarios. This blog post will cover the test
and run blocks, its important components, and how to use it to build automated tests cases for your Terraform configurations.
What is the Terraform test
Block?
The test
block is the top-level container for defining a suite of tests within a .tftest.hcl
(or .tftest.json
) file. It acts as a logical grouping for related test scenarios, each represented by one or more run
blocks.
When you execute the terraform test
command, Terraform discovers all .tftest.hcl
files in your working directory and runs the tests defined within them.
Key Features of the test
block:
- File Naming Convention: Test files must end with
.tftest.hcl
(or.tftest.json
) to be discovered byterraform test
command. - Isolation: Each
test
block creates an ephemeral, isolated testing environment. This means a dedicated working directory, a separate.terraform
folder, and its own state file, which is destroyed after the test run. This prevents tests from interfering with each other or your main configuration. - Declarative: Tests are written in HCL, making them readable, version-controlled, and easily integrated into existing Terraform workflows.
Syntax of the test Block
A test
block typically contains:
- Optional Input Variables: To customize your test.
- Optional Provider Configurations: Including the ability to mock providers.
- One or more
run
blocks: Each representing a specific test scenario or phase.
# example.tftest.hcl
test "my_first_test_suite" {
# 1. Optional: Define input variables for the configuration being tested
variables {
region = "us-east-1"
environment = "test"
}
# 2. Optional: Configure providers for this test run.
# This is crucial for mocking or specific test credentials.
provider "aws" {
region = var.region
# In integration tests, you had provide real credentials or roles here.
# For unit tests, you might mock.
}
# provider "aws" "mock" {
# # This is where you would configure mocking for unit tests.
# # Requires provider-specific mocking capabilities or a mock provider.
# }
# 3. One or more 'run' blocks define individual test scenarios
run "initial_apply_success" {
command = apply # Perform a terraform apply
# Assertions to verify the successful application
assert {
condition = aws_s3_bucket.test_bucket.id != null
error_message = "S3 bucket ID should not be null after apply."
}
assert {
condition = aws_s3_bucket.test_bucket.bucket == "my-test-bucket-12345"
error_message = "S3 bucket name should match the expected value."
}
}
run "plan_no_changes" {
command = plan # Perform a terraform plan after initial apply
# Assert that no changes are planned after the initial apply
assert {
condition = run.initial_apply_success.exit_code == 0
error_message = "Plan should exit cleanly with no errors."
}
assert {
condition = run.initial_apply_success.stderr == ""
error_message = "No errors should be output during plan."
}
}
}
Key Components of test Block
variables block
This block allows you to define input variable values specifically for the configuration being tested within this test
suite. These variables will override any default
values in your main configuration’s variable
blocks.
test "my_test_suite" {
variables {
instance_count = 2
environment = "test-env"
}
# ... runs ...
}
Explanation: This is essential for:
- Unit Testing: Providing specific inputs to a module to test its behavior in isolation.
- Integration Testing: Setting up different scenarios (e.g., small vs. large deployment).
provider block
You can configure providers within a test
block, just like in your main configuration. This is important for controlling how your tests interact with external services.
test "my_aws_test" {
# ...
provider "aws" {
region = "us-west-2"
# For integration tests, ensure credentials are available in the test environment.
# For unit tests, you might use a mock provider if available.
}
# ... runs ...
}
Provider Mocking (Advanced): While core Terraform does not provide a universal mocking framework, some providers (or community mock providers) offer specific ways to “mock” their behavior. This allows you to run unit tests quickly without needing cloud credentials or incurring costs.
run block
The run
block defines an individual test cases or phase within a test
suite. Each run
block executes a specific Terraform command (plan
, apply
, destroy
), records its output, and evaluates assertions against it.
run "my_apply_scenario" {
command = apply # Execute 'terraform apply' for this scenario
# You can pass variables specific to this run block, overriding test.variables
variables {
instance_type = "t2.micro"
}
# Assertions go here
assert {
condition = aws_instance.web_server.id != null
error_message = "Web server should be created."
}
}
Key Arguments of the run
block:
command
: (Required) The Terraform command to execute. Can beplan
,apply
, ordestroy
.variables
: (Optional) A nested block to provide input variables specific to thisrun
block. These overridetest.variables
.assert
: (Optional) A nested block for defining test conditions.condition
: A boolean expression that must evaluate totrue
for the assertion to pass.error_message
: The message displayed if the condition isfalse
.
Accessing Run Outputs: Within subsequent run
blocks in the same test
suite, you can reference the outputs of previous run
blocks using run.<run_block_name>.<attribute>
.
run.my_apply_scenario.stdout
: Standard output of the command.run.my_apply_scenario.stderr
: Standard error of the command.run.my_apply_scenario.exit_code
: The exit code of the command (0 for success).run.my_apply_scenario.<RESOURCE_TYPE>.<RESOURCE_NAME>.<ATTRIBUTE>
: Directly access attributes of resources created or planned in the prior run.run.my_apply_scenario.<OUTPUT_NAME>
: Access output values defined in the configuration under test.
Types of Tests with test Blocks
Unit Testing (Testing Individual Modules)
Unit tests focus on a single module in isolation, often without deploying real infrastructure. This usually involves:
- Mocking: Using a mock provider to simulate cloud resources without actual API calls.
command = plan
: Asserting against the planned changes to verify that the module generates the correct resource attributes.
# modules/my_module/main.tf (module under test)
resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name
acl = var.bucket_acl
}
variable "bucket_name" {}
variable "bucket_acl" {}
output "bucket_arn" { value = aws_s3_bucket.my_bucket.arn }
# modules/my_module/bucket_test.tftest.hcl
test "s3_bucket_unit_test" {
variables {
bucket_name = "test-unit-bucket"
bucket_acl = "private"
}
# This assumes your provider has a mock implementation or you are using a generic mock provider
provider "aws" {
# mock = true
# Conceptual: how you might enable mocking
# Refer provider documentation for support and usage
}
run "plan_for_public_bucket" {
command = plan
variables {
bucket_acl = "public-read" # Override acl for this specific run
}
assert {
condition = plan.aws_s3_bucket.my_bucket.acl.value == "public-read"
error_message = "Planned ACL should be public-read."
}
assert {
condition = plan.aws_s3_bucket.my_bucket.id.value == "test-unit-bucket"
error_message = "Planned bucket name should be 'test-unit-bucket'."
}
}
run "plan_for_private_bucket" {
command = plan # Uses variables from the 'test' block
assert {
condition = plan.aws_s3_bucket.my_bucket.acl.value == "private"
error_message = "Planned ACL should be private."
}
}
}
Integration Testing (Testing Module Interactions or Deployments)
Integration tests deploys ephemeral infrastructure to a real cloud environment (often a dedicated test account). They verify that modules interact correctly and that resources are provisioned as expected.
# root/main.tf (your main configuration that uses modules)
module "network" {
source = "./modules/network"
# ...
}
module "app_server" {
source = "./modules/app_server"
vpc_id = module.network.vpc_id
# ...
}
# root/integration_test.tftest.hcl
test "full_deployment_integration" {
# Ensure you are targeting a test-specific region/account for real deploys
variables {
region = "us-east-1"
environment = "integration-test"
instance_type = "t2.micro"
}
provider "aws" {
region = var.region
# Ensure this identity has permissions to create/destroy resources in the test account
}
run "initial_apply_success" {
command = apply
assert {
condition = module.network.vpc_id != null
error_message = "VPC should be created."
}
assert {
condition = module.app_server.instance_id != null
error_message = "App server should be created."
}
# Can also assert against resource attributes:
assert {
condition = aws_instance.app_server_resource_in_main_config.instance_type == var.instance_type
error_message = "Instance type mismatch."
}
}
run "re_apply_no_changes" {
command = apply # Re-apply to check idempotency
assert {
condition = run.initial_apply_success.stdout != null # Just a basic check
error_message = "Re-apply should output something."
}
# A more robust check might be parsing 'stdout' for "No changes" or checking exit code
}
run "destroy_cleanup" {
command = destroy # Clean up resources after tests
assert {
condition = run.re_apply_no_changes.exit_code == 0
error_message = "Destroy should exit cleanly."
}
}
}
Running Terraform Tests
To run your tests, navigate to the directory containing your .tftest.hcl
files (or a parent directory) and execute:
terraform test
Useful terraform test
flags:
terraform test -filter="my_test_suite"
: Run only specific test suites.terraform test -filter="my_test_suite.initial_apply_success"
: Run a specificrun
block within a suite.terraform test -verbose
: Show verbose output including stdout/stderr fromrun
commands.terraform test -json
: Output results in JSON format (useful for CI/CD).
Best Practices for Terraform Tests
- Isolate Test Environments: Always run integration tests in a dedicated, ephemeral test account or isolated sandbox environments to prevent interference with production or development.
- Granular Tests: Break down complex test suites into smaller, focused
run
blocks. - Clean Up: Ensure
terraform destroy
is part of your integration tests (typically in the lastrun
block) to clean up provisioned resources. - Meaningful Assertions: Write clear
condition
anderror_message
pairs that precisely describe what is being tested and what went wrong. - Use
outputs
for Assertions: Define meaningfuloutput
values in your main configuration, as these are often easier to assert against in tests than deeply nested resource attributes. - CI/CD Integration: Automate
terraform test
execution in your CI/CD pipelines (e.g., on pull requests) to catch regressions early. - Test for “No Changes”: After an initial
apply
, a subsequentplan
orapply
that shows “No changes” is a good test of idempotency. - Balance Test Types: Combine unit tests (for speed and isolation) with integration tests (for real-world verification) for comprehensive coverage.
- Organize Test Files: Place test files logically, e.g., alongside the module they test (
modules/my_module/my_module_test.tftest.hcl
).
Conclusion
Terraform testing capability using the test
and run
blocks is long awaited feature to natively perform unit and integration testing in terraform. It enables you to define and execute comprehensive test cases directly within your HCL, simplifying your testing workflow, reducing the reliance on external tools, and providing immediate feedback on the correctness of your terraform configurations.
Mastering the test
block helps you to validate your configurations, have error free deployments, and ensure that your infrastructure consistently meets its desired state. Start incorporating these tests into your terraform development cycle today.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com