---
page_title: Tests - Configuration Language
description: >-
  Write structured test code for validating your configuration.
---

# Tests

-> **Note:** This testing framework is available in Terraform v1.6.0 and later.

Terraform tests let authors validate that module configuration updates do not introduce breaking changes. Tests run against test-specific, short-lived resources, preventing any risk to your existing infrastructure or state.

## Integration or Unit testing

By default, tests within Terraform create real infrastructure and can run assertions and validations against that infrastructure. This is analogous to integration testing because you are testing Terraform's core functionality by executing operations and validating the infrastructure Terraform creates.

You can override the normal testing behavior by updating the `command` attribute within a [`run`](#run-blocks) block (examples below). By default, each `run` block executes with `command = apply` instructing Terraform to execute a complete `apply` operation against your configuration. Replacing the `command` value with `command = plan` instructs Terraform to _not_ create new infrastructure for this `run` block. This allows test authors to validate logical operations and custom conditions within their infrastructure in a process analogous to unit testing.

Terraform v1.7.0 introduced the ability to mock data returned by the providers during a `terraform test` execution. This can be used to write more detailed and complete unit tests.

## Syntax

Each Terraform test lives in a test file. Terraform discovers test files are based on their file extension: `.tftest.hcl` or `.tftest.json`.

Each test file contains the following root level attributes and blocks:

- One to many [`run`](#run-blocks) blocks.
- Zero to one [`variables`](#variables) block.
- Zero to many [`provider`](#providers) blocks.

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, Terraform processes all the values within these blocks at the beginning of the test operation. We recommend defining your `variables` and `provider` blocks first, at the beginning of the test file.

### Example

The following example demonstrates a simple Terraform configuration that creates an AWS S3 bucket, using an input variable to modify the name. We will create an example test file (below) that validates the buckets name is created as expected.

```hcl
# main.tf

provider "aws" {
    region = "eu-central-1"
}

variable "bucket_prefix" {
  type = string
}

resource "aws_s3_bucket" "bucket" {
  bucket = "${var.bucket_prefix}-bucket"
}

output "bucket_name" {
  value = aws_s3_bucket.bucket.bucket
}
```

The following test file runs a single Terraform `plan` command which creates the S3 bucket, and then validates the logic for calculating the name is correct by checking the actual name matches the expected name.

```hcl
# valid_string_concat.tftest.hcl

variables {
  bucket_prefix = "test"
}

run "valid_string_concat" {

  command = plan

  assert {
    condition     = aws_s3_bucket.bucket.bucket == "test-bucket"
    error_message = "S3 bucket name did not match expected"
  }

}
```

## Run blocks

Each `run` block has the following fields and blocks:

| Field or Block Name       | Description                                                                                                              | Default Value |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `command`                 | An optional attribute, which is either `apply` or `plan`.                                                                | `apply`       |
| `plan_options.mode`       | An optional attribute, which is either `normal` or `refresh-only`.                                                       | `normal`      |
| `plan_options.refresh`    | An optional boolean attribute.                                                                                           | `true`        |
| `plan_options.replace`    | An optional attribute containing a list of resource addresses referencing resources within the configuration under test. |               |
| `plan_options.target`     | An optional attribute containing a list of resource addresses referencing resources within the configuration under test. |               |
| [`variables`](#variables) | An optional `variables` block.                                                                                           |               |
| [`module`](#modules)      | An optional `module` block.                                                                                              |               |
| [`providers`](#providers) | An optional `providers` attribute.                                                                                       |               |
| [`assert`](#assertions)   | Optional `assert` blocks.                                                                                                |               |
| `expect_failures`         | An optional attribute.                                                                                                   |               |

The `command` attribute and `plan_options` block tell Terraform which command and options to execute for each run block. The default operation, if you do not specify a `command` attribute or the `plan_options` block, is a normal Terraform apply operation.

The `command` attribute states whether the operation should be a [`plan`](/terraform/cli/commands/plan) or an [`apply`](/terraform/cli/commands/apply) operation.

The `plan_options` block allows test authors to customize the [planning mode](/terraform/cli/commands/plan#planning-modes) and [options](/terraform/cli/commands/plan#planning-options) they would typically need to edit via command-line flags and options. We cover the `-var` and `-var-file` options in the [Variables](#variables) section.

### Assertions

Terraform run block assertions are [Custom Conditions](/terraform/language/expressions/custom-conditions), consisting of a [condition](/terraform/language/expressions/custom-conditions#condition-expressions) and an [error message](/terraform/language/expressions/custom-conditions#error-messages).

At the conclusion of a Terraform test command execution, Terraform presents any failed assertions as part of a tests passed or failed status.

#### Assertion References

Assertions within tests can reference any existing [named values](/terraform/language/expressions/references) that are available to other custom conditions within the main Terraform configuration.

Additionally, test assertions can directly reference outputs from current and previous `run` blocks. Pulling from the [previous example](#example), this is a valid condition: `condition = output.bucket_name == "test_bucket"`.

## Variables

You can provide values for [Input Variables](/terraform/language/values/variables) within your configuration directly from your test files.

The test file syntax supports `variables` blocks at both the root level and within `run` blocks. Terraform passes all variable values from the test file into all `run` blocks within the file. You can override variable values for a particular `run` block with values provided directly within that `run` block.

Adding to the test file from the [example](#example) above:

```hcl
# variable_precedence.tftest.hcl

variables {
  bucket_prefix = "test"
}

run "uses_root_level_value" {

  command = plan

  assert {
    condition     = aws_s3_bucket.bucket.bucket == "test-bucket"
    error_message = "S3 bucket name did not match expected"
  }

}

run "overrides_root_level_value" {

  command = plan

  variables {
    bucket_prefix = "other"
  }

  assert {
    condition     = aws_s3_bucket.bucket.bucket == "other-bucket"
    error_message = "S3 bucket name did not match expected"
  }

}
```

We've added a second `run` block that specifies the `bucket_prefix` variable value as `other`, overriding the value `test` that is provided by the test file and used during the first `run` block.

### Specify variables with the Command Line or definition files

In addition to specifying variable values via test files, the Terraform `test` command also supports the other typical mechanisms for specifying variable values.

You can specify values for variables across all tests with the [Command Line](/terraform/language/values/variables#variables-on-the-command-line) and with [Variable Definition Files](/terraform/language/values/variables#variable-definitions-tfvars-files).

As with the main configuration direction, Terraform will automatically load any variables defined in the automatic variable files within a test directory. The automatic variable files are `terraform.tfvars`, `terraform.tfvars.json`, and any files that end with `.auto.tfvars` or `.auto.tfvars.json`.

-> **Note:** Variable values loaded from the automatic variable files within a test directory will only apply to tests also defined within the same test directory. Variables defined in all other ways will apply to all tests in a given test run.

This is particularly useful for using sensitive variables values and for configuring providers. Otherwise, testing files could directly expose those sensitive values.

### Variable definition precedence

[Variable Definition Precedence](/terraform/language/values/variables#variable-definition-precedence) remains the same within tests, except for variable values that test files provide. The variables defined in test files take the highest precedence, overriding environment variables, variables files, or command-line input.

For tests defined in a test directory, any variable values defined in automatic variable files from the test directory will override values defined in automatic variable files from the main configuration directory.

### Variable References

Variables you define within `run` blocks can refer to outputs from modules executed in earlier `run` blocks and variables defined at higher precedence levels. Variables defined within the file level `variables` block can only refer to global variables.

For example, the following code block shows how a variable can refer to higher precedence variables and previous run blocks:

```hcl
variables {
  global_value = "some value"
}

run "run_block_one" {
  variables {
    local_value = var.global_value
  }

  # ...
  # Some test assertions should go here.
  # ...
}

run "run_block_two" {
  variables {
    local_value = run.run_block_one.output_one
  }

  # ...
  # Some test assertions should go here.
  # ...
}
```

Above, the `local_value` in `run_block_one` gets its value from the `global_value` variable. This pattern is useful if you want to assign multiple variables the same value. You can specify a variable value once at the file level and then share it with different variables.

In comparison, `local_value` in `run_block_two` takes its value from the output value of `output_one` from `run_block_one`. This pattern is useful for passing values between `run` blocks, particularly if `run` blocks are executing different modules as detailed in the [Modules](#modules) section.

## Providers

You can set or override the required providers within the main configuration from your testing files by using `provider` and `providers` blocks and attributes.

At the root level of a Terraform testing file, you can define [`provider` blocks](/terraform/language/providers/configuration) as if Terraform were creating them [within the main configuration](/terraform/language/providers). Terraform will then pass these provider blocks into its configuration as each `run` block executes.

By default, each provider you specify is directly available within each `run` block. You can customize the availability of providers within a given `run` block by using a `providers` attribute. The behavior and syntax for this block match the behavior of [providers meta-argument](/terraform/language/meta-arguments/module-providers).

If you do not provide provider configuration within a testing file, Terraform attempts to initialize any providers within its configuration using the provider's default settings. For example, any environment variables aimed at configuring providers are still available, and Terraform can use them to create default providers.

Below, we expand on our previous [example](#example) to allow tests, instead of the configuration, to specify the region. In this example, we are going to test the following configuration file:

```hcl
# main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

variable "bucket_prefix" {
  type = string
}

resource "aws_s3_bucket" "bucket" {
  bucket = "${var.bucket_prefix}-bucket"
}

output "bucket_name" {
  value = aws_s3_bucket.bucket.bucket
}
```

We can now define our `provider` blocks within the following test file:

```hcl
# customised_provider.tftest.hcl

provider "aws" {
    region = "eu-central-1"
}

variables {
  bucket_prefix = "test"
}

run "valid_string_concat" {

  command = plan

  assert {
    condition     = aws_s3_bucket.bucket.bucket == "test-bucket"
    error_message = "S3 bucket name did not match expected"
  }

}
```

We can also create a more complex example configuration, that makes use of multiple providers and aliases:

```hcl
# main.tf

terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      configuration_aliases = [aws.secondary]
    }
  }
}

variable "bucket_prefix" {
  default = "test"
  type    = string
}

resource "aws_s3_bucket" "primary_bucket" {
  bucket = "${var.bucket_prefix}-primary"
}

resource "aws_s3_bucket" "secondary_bucket" {
  provider = aws.secondary
  bucket   = "${var.bucket_prefix}-secondary"
}
```

Within our test file we can specify multiple providers:

```hcl
# customised_providers.tftest.hcl

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "secondary"
  region = "eu-central-1"
}

run "providers" {

  command = plan

  assert {
    condition     = aws_s3_bucket.primary_bucket.bucket == "test-primary"
    error_message = "invalid value for primary S3 bucket"
  }

  assert {
    condition     = aws_s3_bucket.secondary_bucket.bucket == "test-secondary"
    error_message = "invalid value for secondary S3 bucket"
  }
}
```

It is also possible to define specific providers you want to use in specific `run` blocks:

```hcl
# main.tf

terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      configuration_aliases = [aws.secondary]
    }
  }
}

data "aws_region" "primary" {}

data "aws_region" "secondary" {
  provider = aws.secondary
}

variable "bucket_prefix" {
  default = "test"
  type    = string
}

resource "aws_s3_bucket" "primary_bucket" {
  bucket = "${var.bucket_prefix}-${data.aws_region.primary.name}-primary"
}

resource "aws_s3_bucket" "secondary_bucket" {
  provider = aws.secondary
  bucket   = "${var.bucket_prefix}-${data.aws_region.secondary.name}-secondary"
}
```

Our test file can pass in specific providers for each different `run` block:

```hcl
# customised_providers.tftest.hcl

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "secondary"
  region = "eu-central-1"
}

provider "aws" {
  alias  = "tertiary"
  region = "eu-west-2"
}

run "default_providers" {

  command = plan

  assert {
    condition     = aws_s3_bucket.primary_bucket.bucket == "test-us-east-1-primary"
    error_message = "invalid value for primary S3 bucket"
  }

  assert {
    condition     = aws_s3_bucket.secondary_bucket.bucket == "test-eu-central-1-secondary"
    error_message = "invalid value for secondary S3 bucket"
  }
}

run "customised_providers" {

  command = plan

  providers = {
    aws           = aws
    aws.secondary = aws.tertiary
  }

  assert {
    condition     = aws_s3_bucket.primary_bucket.bucket == "test-us-east-1-primary"
    error_message = "invalid value for primary S3 bucket"
  }

  assert {
    condition     = aws_s3_bucket.secondary_bucket.bucket == "test-eu-west-2-secondary"
    error_message = "invalid value for secondary S3 bucket"
  }
}
```

> **Note:** When running tests with `command = apply`, switching providers between `run` blocks can result in failed operations and tests because resources created by one provider definition will be unusable when modified by a second.

From Terraform v1.7.0, `provider` blocks can also reference test file variables and run block outputs. This means the testing framework can retrieve credentials and other setup information from one provider and use this when initializing a second.

In the following example, the `vault` provider is initialized first, and then used within a setup module to extract credentials for the `aws` provider. For more information on setup modules, see [Modules](#modules).

```hcl

provider "vault" {
  # ... vault configuration ...
}

provider "aws" {
  region     = "us-east-1"

  # The `aws` provider can reference the outputs of the "vault_setup" run block.
  access_key = run.vault_setup.aws_access_key
  secret_key = run.vault_setup.aws_secret_key
}

run "vault_setup" {
  module {
    # This module should only include reference to the Vault provider. Terraform
    # will automatically work out which providers to supply based on the module
    # configuration. The tests will error if a run block requires access to a
    # provider that references outputs from a run block that has not executed.
    source = "./testing/vault-setup"
  }
}

run "use_aws_provider" {
  # This run block can then use both the `aws` and `vault` providers, as the
  # previous run block provided all the data required for the `aws` provider.
}
```

## Modules

You can modify the module that a given `run` block executes.

By default, Terraform executes the given command against the configuration being tested for each `run` block. Terraform tests the configuration within the directory you execute the `terraform test` command from (or the directory you point to with the `-chdir` argument). Each `run` block also allows the user to change the targeted configuration using the `module` block.

Unlike the traditional [`module` block](/terraform/language/modules/syntax), the `module` block within test files _only_ supports the [`source`](/terraform/language/modules/syntax#source) attribute and the [`version`](/terraform/language/modules/syntax#version) attribute. The remaining attributes that are typically supplied via the traditional `module` block should be supplied by the alternate attributes and blocks within the `run` block.

> **Note:** Terraform test files only support [local](/terraform/language/modules/sources#local-paths) and [registry](/terraform/language/modules/sources#terraform-registry) modules within the `source` attribute.

All other blocks and attributes within the `run` block are supported when executing an alternate module, with `assert` blocks executing against values from the alternate module. This is discussed more in [Modules State](#modules-state).

Two example use cases for the `modules` block within a testing file are:

1. A setup module that creates the infrastructure the main configuration requires for testing.
2. A loading module to load and validate secondary infrastructure (such as data sources) that are not created directly by the main configuration being tested.

The following examples demonstrate both of these use cases.

First, we have a module that will create and load several files into an already created S3 bucket. This is the configuration we want to test.

```hcl
# main.tf

variable "bucket" {
  type = string
}

variable "files" {
  type = map(string)
}

data "aws_s3_bucket" "bucket" {
  bucket = var.bucket
}

resource "aws_s3_object" "object" {
  for_each = var.files

  bucket = data.aws_s3_bucket.bucket.id
  key = each.key
  source = each.value

  etag = filemd5(each.value)
}
```

Second, we have a setup module that will create the S3 bucket, so it is available to the configuration under test.

```hcl
# testing/setup/main.tf

variable "bucket" {
  type = string
}

resource "aws_s3_bucket" "bucket" {
  bucket = var.bucket
}
```

Third, we have a loading module, that will load the files in the s3 bucket. This is a fairly contrived example, as it is definitely possible just to validate the files directly when they are created in the module under test. It is, however, good for demonstrating the use case.

```hcl
# testing/loader/main.tf

variable "bucket" {
  type = string
}

data "aws_s3_objects" "objects" {
  bucket = var.bucket
}
```

Finally, we have the test file itself which configures everything and calls out to the various helper modules we have created.

```hcl
# file_count.tftest.hcl

variables {
  bucket = "my_test_bucket"
  files = {
    "file-one.txt": "data/files/file_one.txt"
    "file-two.txt": "data/files/file_two.txt"
  }
}

provider "aws" {
  region = "us-east-1"
}

run "setup" {
  # Create the S3 bucket we will use later.

  module {
    source = "./testing/setup"
  }
}

run "execute" {
  # This is empty, we just run the configuration under test using all the default settings.
}

run "verify" {
  # Load and count the objects created in the "execute" run block.

  module {
    source = "./testing/loader"
  }

  assert {
    condition = length(data.aws_s3_objects.objects.keys) == 2
    error_message = "created the wrong number of s3 objects"
  }
}
```

### Modules state

While Terraform executes a `terraform test` command, Terraform maintains at least one, but possibly many, state files within memory for each test file.

There is always at least one state file that maintains the state of the main configuration under test. This state file is shared by all `run` blocks that do not have a `module` block specifying an alternate module to load.

Additionally, there is one state file per alternate module that Terraform loads. An alternate module state file is shared by all `run` blocks that execute the given module.

The Terraform team is interested in any use cases requiring manual state management or the ability to execute different configurations against the same state within the `test` command. If you have a use case, please file an [issue](https://github.com/hashicorp/terraform/issues/new/choose) and share it with us.

The following example uses comments to explain where the state files for each `run` block originate. In the below example Terraform creates and manages a total of three state files. The first state file is for the main configuration under test, the second for the setup module, and the third for the loader module.

```hcl
run "setup" {

  # This run block references an alternate module and is the first run block
  # to reference this particular alternate module. Therefore, Terraform creates
  # and populates a new empty state file for this run block.

  module {
    source = "./testing/setup"
  }
}

run "init" {

  # This run block does not reference an alternate module, so it uses the main
  # state file for the configuration under test. As this is the first run block
  # to reference the main configuration, the previously empty state file now
  # contains the resources created by this run block.

  assert {
    # In practice we'd do some interesting checks and tests here but the
    # assertions aren't important for this example.
  }

  # ... more assertions ...
}

run "update_setup" {

  # We've now re-referenced the setup module, so the state file that was created
  # for the first "setup" run block will be reused. It will contain any
  # resources that were created as part of the other run block before this run
  # block executes and will be updated with any changes made by this run block
  # after.

  module {
    source = "./testing/setup"
  }

  variables {
    # In practice, we'd likely make some changes to the module compared to the
    # first run block here. Otherwise, there would be no point recalling the
    # module.
  }
}

run "update" {

  # As with the "init" run block, we are executing against the main configuration
  # again. This means we'd load the main state file that was initially populated
  # by the "init" run block, and any changes made by this "run" block will be
  # carried forward to any future run blocks that execute against the main
  # configuration.

  # ... updated variables ...

  # ... assertions ...
}

run "loader" {

  # This run block is now referencing our second alternate module so will create
  # our third and final state file. The other two state files are managing
  # resources from the main configuration and resources from the setup module.
  # We are getting a new state file for this run block as the loader module has
  # not previously been referenced by any run blocks.

  module {
    source = "./testing/loader"
  }
}
```

#### Modules Cleanup

At the conclusion of a test file, Terraform attempts to destroy every resource it created during the execution of that test file. When Terraform loads alternate modules, the order in which Terraform destroys those objects in is important. For example, in the first [Modules](#modules) example, Terraform could not destroy the resources created in the "setup" `run` block before the objects created in the "execute" `run` block, because the S3 bucket we created in the "setup" step can not be destroyed while it contains objects.

Terraform destroys resources in reverse `run` block order. In the most recent [example](#modules-state), there are three state files. One for the main state, one for the `./testing/loader` module, and one for the `./testing/setup` module. The `./testing/loader` state file would be destroyed first as it was referenced most recently by the last run block. The main state file would be destroyed second as it was referenced by the "update" `run` block. The `./testing/setup` state file would then be destroyed last.

Note, that the first two `run` blocks "setup" and "init", do nothing during the destroy operations as their state files are used by later run blocks and have already been destroyed.

If you use a single setup module as an alternate module, and it executes first, or you use no alternate modules, then the order of destruction does not affect you. Anything more complex may require careful consideration to make sure the destruction of resources can complete automatically.

## Expecting failures

By default, if any [Custom Conditions](/terraform/language/expressions/custom-conditions), including `check` block assertions, fail during the execution of a Terraform test file then the overall command reports the test as a failure.

However, it is a common testing paradigm to want to test failure cases. Terraform supports the `expect_failures` attribute for this use case.

In each `run` block the `expect_failures` attribute can provide a list of checkable objects (resources, data sources, check blocks, input variables, and outputs) that should fail their custom conditions. The test passes if the checkable objects you specify report an issue, and the test fails overall if they do not.

You can still write assertions alongside an `expect_failures` block, but you should be mindful that all custom conditions, except check block assertions, halt the execution of Terraform. This still applies during test execution, so your assertions should only consider values that you are sure will be computed before the checkable object is due to fail. You can manage this using references, or the `depends_on` meta-argument within your main configuration.

This also means that, with the exception of `check` blocks, you can only reliably include a single checkable object. We support a list of checkable objects within the `expect_failures` attribute purely for `check` blocks.

A quick example below demonstrates testing the `validation` block on an input variable. The configuration file accepts a single input variable that must be even number.

```hcl
# main.tf

variable "input" {
  type = number

  validation {
    condition = var.input % 2 == 0
    error_message = "must be even number"
  }
}
```

The test file contains two run blocks. One that validates that our custom condition passes on an even number and one that validates our custom condition fails on an odd number.

```hcl
# input_validation.tftest.hcl

variables {
  input = 0
}

run "zero" {
  # The variable defined above is even, so we expect the validation to pass.

  command = plan
}

run "one" {
  # This time we set the variable is odd, so we expect the validation to fail.

  command = plan

  variables {
    input = 1
  }

  expect_failures = [
    var.input,
  ]
}
```

> **Note**: Terraform only expects failures in the operation specified by the `command` attribute of the `run` block.

Be careful when using `expect_failures` in `run` blocks with `command = apply`. A `run` block with `command = apply` that expects a custom condition failure will fail overall if that custom condition fails during the plan.

This is logically consistent, as the `run` block is expecting to be able to run an apply operation but can not because the plan failed. It is also potentially confusing, as you will see the failure in the diagnostics as the reason the test failed, even though that failure was marked as being expected.

There are instances when Terraform does not execute a custom condition during the planning stage, because that condition is relying on computed attributes that are only available after Terraform creates the referenced resource. In these cases, you could use an `expect_failures` block alongside a `command = apply` attribute and value. However, in most cases we recommend only using `expect_failures` alongside `command = plan` operations.

> **Note**: Expected failures only apply to user-defined custom conditions.

Other kinds of failure _besides_ the specified expected failures in the checkable object still result in the overall test failing. For example, a variable that expects a boolean value as input fails the surrounding test if Terraform provides the wrong kind of value, even if that variable is included in an `expect_failures` attribute.

The `expect_failures` attribute is included to allow authors to test their configuration and any logic defined within. A type mismatch, as in the previous example, is not something Terraform authors should have to worry about testing as Terraform itself will handle enforce type constraints. As such, you can only `expect_failures` in custom conditions.
