Terraform testing is the process of automatically verifying that your Terraform configurations are syntactically correct, functionally accurate, and compliant with security and operational policies. It spans multiple layers: static analysis (linting, validation), unit and integration tests using frameworks like Terratest or Terraform’s native terraform test command, and drift detection to catch real-world changes.
A solid testing strategy reduces deployment risk and makes infrastructure changes reproducible and auditable.
In this article, we will take a look at Terraform testing strategies using different types of commonly used testing, tools and platforms. Let’s jump straight in!
We will cover:
What is the Terraform test file syntax?
Terraform discovers test files inside your configuration by checking the following extensions:
- .tftest.hcl
- .tftest.json
Every test file is composed of root-level attributes and blocks as follows:
- One or more run blocks
- One or zero variables block
- Zero to many provider blocks.
Terraform runs blocks sequentially, mimicking a sequence of Terraform commands in the configuration directory. The sequence in which the variables and provider blocks are arranged doesn’t matter, as Terraform evaluates all values within these blocks at the start of the testing process.
For clarity, it is advisable to place your variables and provider blocks at the start of the test file.
Terraform test file example
The run block command can be either plan or apply, if you omit to add it, the default value is a plan.
run "example_test" {
command = plan
// Define any variables required for your test
variables {
example_variable = "value"
}
// Assertions to validate the test outcome
assert {
condition = example_resource.attribute == var.example_variable
error_message = "Test failed: the attribute value was not as expected."
}
// Optionally, you can specify providers if your test requires it
provider "aws" {
region = "us-west-2"
}
}Inside the run block, you can include optional variables and assertions, and you can use the variables in assertions as you would any Terraform variable by prefixing them with var. Variables can also be defined globally (outside the run block), or just inside the run block for them to be accessed only by that particular run block.
Assertions in run blocks check a condition, and if that condition fails, it will be presented at the end of the Terraform verification.
You can define how many providers you want and specify them as part of the run block or outside of it.
What is the Terraform test command?
The terraform test command is used to run all the test files defined in your configuration.
What does the Terraform test command do?
The terraform test command reads the Terraform test files defined in your configuration files by looking at all the files that have the .tftest.json and .tftest.hcl extensions and executes the tests defined in those files. By default, Terraform will search for these files in the current directory and also in the specified testing directory (by default, it is tests).
Terraform test command usage options
The terraform test command can receive a couple of optional attributes:
-test-directory=<directory> – overrides the directory that Terraform looks into for test files-json– displays JSON output for your testing results-verbose– prints out the plan or state for each run block within a test file-filter=testfile– will limit the test operation on specific testfiles only
Terraform test command usage examples
terraform test -filter=tests/my_test.tftest.hcl– this will test only the tests/my_test.tftest.hclterraform test -verbose– will print out the plan or state for each run blockterraform test -test-directory=my_tests– will run the tests from the current directory and my_tests directory
Terraform testing strategies
As with any code, you should implement some testing strategies to help validate your code and ensure that your changes do not cause any unexpected issues or break existing functionality.
Common Terraform testing strategies include:
- Integration testing
- Unit testing
- End-to-end (E2E) testing
- Linting
- Compliance testing
- Drift testing
In this article, we will take a look at each and how to use them with Terraform code.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Integration testing
Integration testing involves testing your entire infrastructure configuration, including any dependencies on other resources, to ensure that they work together correctly. Terraform has built-in dependency mapping and management that can be utilized to make sure the changes being made are as expected.
You can use tools like Terratest or Kitchen-Terraform for integration testing.
The following tools can also be used in the workflow, and including them in the CI pipeline will form Integration Testing:
- Terraform fmt — to format the code correctly.
- Terraform validate — to verify the syntax.
- Terraform plan — to verify the config file will work as expected.
- TFLint — to verify the contents of the configuration as well as the syntax and structure, also checks account limits (e.g. that a VM instance type is valid, and that the limit on the number of VMs in Azure has not been reached).
- Kitchen-Terraform — Kitchen-Terraform is an open-source tool that provides a framework for writing automated tests that validate the configuration and behavior of Terraform code, including testing for errors, resource creation, destruction, and validation of outputs. Kitchen-Terraform uses a combination of Ruby, Terraform, and Test Kitchen to spin up infrastructure in a sandbox environment, run tests, and destroy the infrastructure once testing is complete.
- Terratest — Terratest is an open-source testing framework for testing Terraform that can also be used to test Kubernetes, Docker, and Packer, amongst others. Terratest enables automated testing of infrastructure code in a sandbox environment to validate that it meets requirements, functions as expected, and is reliable. Tests are written in Go, and it provides a rich set of testing functions that allow the user to automate the entire testing process, including provisioning resources, running tests, and cleaning up resources. Terratest also provides built-in support for popular testing frameworks like Ginkgo and Gomega.
How to use Terratest
In the Terratest example below, a function is created to test the creation of an Azure storage account in a resource group, and a container in the storage account, then verifies that the storage account and container exist. The resources are then cleaned up after the test is complete.
package test
import (
"context"
"fmt"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/gruntwork-io/terratest/modules/azure"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/testing"
)
func TestAzureStorageAccount(t *testing.T) {
t.Parallel()
// Set the Azure subscription ID and resource group where the Storage Account will be created
uniqueID := random.UniqueId()
subscriptionID := "YOUR_AZURE_SUBSCRIPTION_ID"
resourceGroupName := fmt.Sprintf("test-rg-%s", uniqueID)
// Create the resource group
azure.CreateResourceGroup(t, resourceGroupName, "uksouth")
// Set the Storage Account name and create the account
accountName := fmt.Sprintf("teststorage%s", uniqueID)
accountType := storage.StandardZRS
location := "uksouth"
account := azure.CreateStorageAccount(t, subscriptionID, resourceGroupName, accountName, accountType, location)
// Create a container in the Storage Account
ctx := context.Background()
client, err := storage.NewBasicClient(accountName, azure.GenerateAccountKey(t, accountName))
if err != nil {
t.Fatalf("Failed to create Storage Account client: %v", err)
}
service := client.GetBlobService()
containerName := fmt.Sprintf("testcontainer%s", uniqueID)
if _, err := service.CreateContainer(ctx, containerName, nil); err != nil {
t.Fatalf("Failed to create container: %v", err)
}
// Check if the container exists
testing.WaitForTerraform(ctx, terraformOptions, nil)
// Cleanup resources
defer azure.DeleteResourceGroup(t, resourceGroupName)
}Unit testing
Unit testing involves testing individual modules or resources in isolation to ensure that they work as expected. This can again be done using tools like Terratest or using Terraform’s built-in testing functionality.
Unit Testing with the Native Test Framework
Both Terraform (v1.6+) and OpenTofu support native unit testing using HCL-based .tftest.hcl files, no additional language or framework required. Unlike Terratest, which requires writing tests in Go, the native framework lets you write tests in the same declarative HCL you already use for your infrastructure code.
For unit tests, you use command = plan inside a run block so no real infrastructure is created. The example below tests that an EC2 instance variable validation works correctly:
# tests/instance_type.tftest.hcl
run "valid_instance_type" {
command = plan
variables {
instance_type = "t2.micro"
}
assert {
condition = var.instance_type == "t2.micro"
error_message = "Expected instance type t2.micro."
}
}
run "invalid_instance_type" {
command = plan
variables {
instance_type = "m5.24xlarge"
}
expect_failures = [var.instance_type]
}OpenTofu also supports a .tofutest.hcl file extension for tests that should only run on OpenTofu — if both main.tftest.hcl and main.tofutest.hcl exist in the same directory, OpenTofu will load only the .tofutest.hcl file, which is useful for module authors who want to support both tools.
Note on CDKTF: AWS CDK for Terraform (CDKTF) was sunset and archived by HashiCorp on December 10, 2025. The repository is now read-only with no ongoing maintenance. If you were using CDKTF with Jest or unittest for Terraform testing, the native .tftest.hcl framework is the recommended path forward.
End-to-End (E2E) testing
End-to-end testing involves testing your infrastructure configuration in a production-like environment to ensure that it works as expected in a real-world scenario. This can be done using tools like Terratest or manual testing in a staging environment.
Continuing with the Azure storage account example, let’s look at how we can use Terratest to implement end-to-end testing.
- Create a Terraform configuration file that defines an Azure Storage account and container:
main.tf
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "test" {
name = "test-resource-group"
location = "uksouth"
}
resource "azurerm_storage_account" "test" {
name = "teststorageaccount"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
account_tier = "Standard"
account_replication_type = "ZRS"
tags = {
environment = "test"
}
}
resource "azurerm_storage_container" "test" {
name = "testcontainer"
storage_account_name = azurerm_storage_account.test.name
container_access_type = "private"
}- Create a Terratest test file to deploy the Terraform configuration and validate the created Azure Storage account exists, along with testing that the container has the correct name and access type.
storage_test.go
package test
import (
"context"
"fmt"
"os"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
"github.com/gruntwork-io/terratest/modules/azure"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestAzureStorageAccountAndContainer(t *testing.T) {
t.Parallel()
// Define the Terraform options with the desired variables
terraformOptions := &terraform.Options{
// The path to the Terraform code to be tested.
TerraformDir: "../",
// Variables to pass to our Terraform code using -var options
Vars: map[string]interface{}{
"environment": "test",
},
}
// Deploy the Terraform code
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Retrieve the Azure Storage account and container information using the Azure SDK
storageAccountName := terraform.Output(t, terraformOptions, "storage_account_name")
storageAccountGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
containerName := terraform.Output(t, terraformOptions, "container_name")
// Authenticate using the environment variables or Azure CLI credentials
azClient, err := azure.NewClientFromEnvironmentWithResource("https://storage.azure.com/")
if err != nil {
t.Fatal(err)
}
// Get the container properties
container, err := azClient.GetBlobContainer(context.Background(), storageAccountGroupName, storageAccountName, containerName)
if err != nil {
t.Fatal(err)
}
// Check that the container was created with the expected configuration
assert.Equal(t, "testcontainer", *container.Name)
assert.Equal(t, "private", string(container.Properties.PublicAccess))
fmt.Printf("Container %s was created successfully\n", containerName)
}- Run the test by navigating to the directory where the go file is saved and run the following command.
go test -vContract testing
Contract testing checks if a configuration using a Terraform module gives properly formatted inputs. They verify the compatibility between the expected and actual inputs of a Terraform module, ensuring the module is used as intended by its author.
These tests utilize input validations, along with preconditions and postconditions, to check input combinations and errors, facilitating clear communication from the module creator to the users about the intended use of the module:
variable "instance_type" {
type = string
description = "The instance type for the EC2 instance."
validation {
condition = contains(["t2.micro", "t2.small", "t3.micro"], var.instance_type)
error_message = "The instance type must be t2.micro, t2.small, or t3.micro."
}
}
resource "aws_instance" "example" {
instance_type = var.instance_type
}In the above example, we ensure that an instance type can be only t2.micro, t2.small or t3.micro.
Mocks
Test mocking is a beta feature that lets you mock providers, resources and datasource for your tests, which lets you use terraform test without creating infrastructure or requiring credentials. They can be only used with terraform test and generate fake data for all the computed attributes.
Mock Providers
Mock Providers are used to create a dummy version of a provider, enabling the testing of Terraform configurations without actual cloud interactions. They can be used instead of a traditional provider block in your test, and they share the global namespace. Terraform won’t distinguish between a real and a mocked provider.
They will be defined with a “mock_provider” entry in your test files like so:
mock_provider "aws" {}Note: Test mocking is available in Terraform v1.7.0 and later. This feature is in beta.
Overrides
Overrides can modify existing configurations for testing purposes, allowing for the alteration of resource attributes, data source attributes or even provider settings.
Each override block identifies the target—be it a resource, data source, or module—through a target attribute. For modules, an outputs attribute is used within override_module blocks to specify overrides. In contrast, override_resource and override_data blocks use a values attribute to define the overrides for resources and data sources, respectively.
Repeated blocks and nested attributes
Repeated blocks and nested attributes can also be tested with mocks to ensure that configurations with complex structures behave as expected, even when actual resources are not being manipulated.
For example, for every block inside of a resource that supports a dynamic block, you can build mock attributed that will mimic this behavior
Linting
Linting is very important in every programming language and even though HCL is a declarative language, linting can also ensure that your code is written correctly.
In the Terraform context, linting refers to the process of analyzing code to detect syntax errors, enforce style guidelines, and identify potential issues before doing an actual terraform plan. This step is crucial for maintaining code quality and consistency, and it enhances collaboration. Tools designed for Terraform, such as tflint, offer customizable rulesets that cater to different project needs and standards, facilitating the automation of code review processes and integration into CI pipelines.
- Terraform fmt: This is a built-in Terraform command that should be the first port of call. The command formats your Terraform code based on a set of standard formatting rules.
Use terraform fmt -check and terraform validate to format and validate the correctness of your Terraform configuration.
- TFLint: This is a popular open-source tool that checks for syntax errors, best practices, and code style consistency. Once installed, simply run it using the command:
tflint /path/to/terraform/code- Checkov: This is an open-source static analysis tool for Terraform that checks for security and compliance issues in your Terraform code. Install it using the Python package manager pip and run it using the command below:
pip install checkov
checkov -d /path/to/terraform/codeCheckov will identify security issues and provides recommendations for how to fix the issue, along with the location of the relevant code, such as publically accessible storage accounts.
- Terrascan: This open-source tool performs static code analysis to identify security vulnerabilities and compliance violations in Terraform code. Example output is shown below for a publically accessible storage account:
=== [azure_storage_account] ===
Rules:
Risky network access configuration for storage account
Rule ID: AWS_3_2
Description: A storage account with unrestricted network access configuration may result in unauthorized access to the account and its data.
Severity: CRITICAL
Tags: [network,storage]
Status: FAILED
Resource:Check out the list of other popular tools used in Terraform-managed deployments.
Compliance testing
terraform-compliance enables you to write a set of conditions in YAML files that must be met and test your code against them.
It can easily be installed using pip and run using the command shown below:
pip install terraform-compliance
terraform-compliance -p /path/to/policy/file.yaml -f /path/to/terraform/codeFor example, the YAML file below specifies that Azure Storage Account should not be publicly accessible:
controls:
- name: azure-storage-not-publicly-accessible
description: Azure Storage Account should not be publicly accessible
rules:
- azure_storage_account:
public_access_allowed: FalseDrift testing
Terraform will natively test for drift between your code and the real infrastructure when terraform plan is run. Terraform will compare the current state of your infrastructure to the state saved in the state file.
If there are any differences, Terraform will display an execution plan that shows how to update your infrastructure to match your configuration.
You can also make use of driftctl which is a free open-source tool that can report on infrastructure drift. Example output from the tool is shown below:
Found 11 resource(s) – 73% coverage
– 8 managed by terraform
– 2 not managed by terraform
– 1 found in terraform state but missing on the cloud providerRunning terraform plan manually works fine for catching drift on demand, but it doesn’t scale well across multiple stacks. If you’re managing several environments, you need something that monitors continuously and alerts you when drift happens — not something you remember to run.
Spacelift’s drift detection runs on a configurable schedule across all your stacks, surfaces drift in a single dashboard, and lets you remediate directly from the UI. Setup is as simple as configuring a cron job. You can read more in the documentation.
When to use each Terraform testing strategy?
Not every project needs every type of test. The table below gives you a quick reference for deciding what to use and when.
| Strategy | Use it when… | Skip it when… |
| Linting | Always — every project, every pipeline | — |
| Unit testing | Testing module logic, variable validation, conditional expressions | Your config has no reusable modules or custom logic |
| Contract testing | You publish modules for others to consume | You only consume modules, never publish them |
| Integration testing | Testing that multiple resources work together correctly | Cost or time constraints make real infrastructure impractical |
| E2E testing | Validating a full environment before promoting to production | Early development stages where configs change frequently |
| Compliance testing | Enforcing security policies across teams or at scale | Single-developer projects with no governance requirements |
| Drift testing | Long-lived infrastructure that may be modified outside of IaC | Ephemeral environments that are always rebuilt from scratch |
A good rule of thumb is to think in layers:
┌───────────────┐
│ E2E Testing │ ← Pre-production / release gates
├───────────────┤
│ Integration │ ← PR merges / nightly
├───────────────┤
│ Unit/Contract │ ← Every commit
├───────────────┤
│ Linting │ ← Pre-commit hooks
└───────────────┘- Linting (
terraform fmt,terraform validate, TFLint) should run as a pre-commit hook or the first step in your CI pipeline. It’s near-instant and catches the most common errors early. - Unit and contract tests using
terraform testwithcommand = plancreate no real infrastructure, so they’re fast enough to run on every pull request. - Integration tests create real resources and can take minutes to hours depending on your provider. Run these on PR merges or nightly, and always destroy resources immediately afterward to keep costs down.
- E2E tests are the most expensive and should act as a gate before promoting changes to staging or production.
- Compliance and drift testing sit outside the standard commit/merge cycle. Compliance tests fit best as a policy enforcement step in CI. Drift detection should run on a schedule against live environments — you can read more about how Spacelift handles this in the drift detection section below.
Key points
By combining the testing strategies outlined in this article and adopting a holistic approach, you can ensure that your Terraform code is well-tested and reliable, and that changes are deployed quickly and safely.
Spacelift is designed for your whole team. Everyone works in the same space, supported by policy as code that enforces access controls, security guardrails, and compliance standards — so you can manage your infrastructure more efficiently without compromising on safety.
Read more about integrating security tools with Spacelift. And if you want to learn more about Spacelift, create a free account or book a demo with one of our engineers.
Ship infrastructure as fast as developers code
Build more complex workflows with Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization, and more.
Frequently asked questions
What is the difference between terraform test and Terratest?
terraform test is Terraform’s built-in framework (v1.6+) where tests are written in HCL. Terratest is a third-party Go-based framework from Gruntwork that offers more flexibility for complex E2E scenarios but has a steeper learning curve.
What is the difference between terraform plan and terraform test?
terraform plan previews what changes Terraform would make. terraform test runs assertions against your configuration to verify it behaves as expected, and supports mocking and multi-step run blocks.
Can I use terraform test with OpenTofu?
Yes. OpenTofu supports the same .tftest.hcl format via the tofu test command. It also adds a .tofutest.hcl extension for OpenTofu-specific tests, useful when maintaining modules that target both tools.
What version of Terraform introduced the terraform test command?
General availability landed in Terraform v1.6.0 (October 2023). Mock providers were introduced as a beta feature in v1.7.0.
HashiCorp Developer | Terraform Docs. Tests – Configuration Language. Accessed: 21 October 2025
HashiCorp Developer | Terraform Docs. Write Terraform Tests. Accessed: 21 October 2025
HashiCorp Blog. Testing HashiCorp Terraform. Accessed: 21 October 2025
