| --- |
| 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: |
| |
| - Zero to one [`test`](#test-block) blocks. |
| - One to many [`run`](#run-blocks) blocks. |
| - Zero to one [`variables`](#variables) block. |
| - Zero to many [`provider`](#providers) blocks. |
| |
| By default, Terraform executes `run` blocks sequentially. To execute `run` blocks in parallel, refer to [`Parallel execution`](#parallel-execution). Each `run` block simulates 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" |
| } |
| |
| } |
| ``` |
| |
| ## Test block |
| |
| The optional `test` block defines the configuration of the test file, allowing you to configure how the framework |
| executes its runs. The block has the following fields: |
| |
| | Field or Block Name | Description | Default Value | |
| | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------- | |
| | `parallel` | An optional boolean attribute. If `true`, Terraform executes all eligible `run` blocks simultaneously. Refer to [Parallel execution](#parallel-execution) for more information. | `false` | |
| |
| ### Example usage |
| |
| ```hcl |
| # with_config.tftest.hcl |
| test { |
| parallel = true |
| } |
| ``` |
| |
| ## 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. | | |
| | `state_key` | An optional attribute. | | |
| | `parallel` | An optional boolean attribute. | `false` | |
| |
| 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. |
| |
| The `state_key` allows for fine-grained control over which internal state file Terraform uses for a given run block. Refer to [Modules State](#modules-state) for more information. |
| |
| The `parallel` attribute lets you run multiple `run` blocks in parallel. By default, this attribute is set to `false`. When set to `true`, Terraform attempts to execute the `run` block in parallel with other `run` blocks that also have the `parallel` attribute set to `true` and are not dependent on each other. |
| |
| ### Example usage |
| |
| ```hcl |
| # with_config.tftest.hcl |
| test { |
| parallel = true |
| } |
| |
| variables { |
| bucket_prefix = "test" |
| } |
| |
| run "first" { |
| assert { |
| condition = aws_s3_bucket.bucket.bucket == "test-bucket" |
| error_message = "S3 bucket name did not match expected" |
| } |
| } |
| |
| run "second" { |
| assert { |
| condition = aws_s3_bucket.bucket.bucket == "test-bucket" |
| error_message = "S3 bucket name did not match expected" |
| } |
| } |
| |
| run "third" { |
| parallel = false |
| assert { |
| condition = aws_s3_bucket.bucket.bucket == "test-bucket" |
| error_message = "S3 bucket name did not match expected" |
| } |
| } |
| ``` |
| |
| In the above example, the `first` and `second` `run` blocks implicitly have `parallel` set to `true` since the `test` block enables parallel runs. The `third` `run` block sets `parallel` to `false` to override the global setting. |
| |
| ### 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. Terraform assigns each internal state file a state key that it uses internally to track the state file. The state key is a unique identifier for the state file and you can override it with the `state_key` attribute of a `run` block. |
| |
| 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. By default, there is also 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. |
| |
| You can override this default behavior with the `state_key` attribute and force Terraform to use a specific state file for a given `run` block. This is useful when you want to share state between `run` blocks that do not reference the same module. |
| |
| The following example uses comments to explain where the state files for each `run` block originate using the default behavior. 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" |
| } |
| } |
| ``` |
| |
| The following example uses the `state_key` attribute to force Terraform to use the same state file for different `run` blocks. In the example below, Terraform creates and manages a single state file that is shared by both the "setup" and "init" run blocks, even though they are loading configuration from separate sources. |
| |
| ```hcl |
| run "setup" { |
| state_key = "main" |
| |
| module { |
| source = "./testing/setup" |
| } |
| } |
| |
| run "init" { |
| |
| # By setting the state key to "main" we are telling Terraform to use the same |
| # state file for this run block as the "setup" run block. This means that the |
| # resources created by the "setup" run block will be available to the |
| # configuration in this run block. |
| state_key = "main" |
| |
| assert { |
| # In practice we'd do some interesting checks and tests here but the |
| # assertions aren't important for this example. |
| } |
| |
| # ... more assertions ... |
| } |
| ``` |
| |
| #### 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. |
| |
| ## Parallel execution |
| |
| By default, Terraform executes `run` blocks sequentially. However, you can set the `parallel` attribute to `true` in the optional `test` block or in individual `run` blocks to enable parallel execution. |
| |
| Two or more `run` blocks can be executed in parallel if: |
| |
| - They do not reference outputs from each other. |
| - They do not share the same state file. The state file is determined by the state key or the module source when the state key is not set. If two `run` blocks that share the same module configuration, they must specify different state keys to be executed in parallel. |
| - They both have the `parallel` attribute set to `true`. |
| |
| ### Example usage |
| |
| ```hcl |
| # parallel.tftest.hcl |
| |
| test { |
| // This would set the parallel flag to true in all runs |
| parallel = true |
| } |
| |
| variables { |
| foo = "foo" |
| } |
| |
| |
| run "primary_db" { |
| // This is the first run block, and it is available to be executed right away. |
| state_key = "primary" |
| module { |
| source = "./setup" |
| } |
| |
| variables { |
| input = "foo" |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "bad" |
| } |
| } |
| |
| run "secondary_db" { |
| // This run block can be executed in parallel with the `primary_db` run block, because it does not reference its |
| // output and has a different state key. |
| state_key = "secondary" |
| module { |
| source = "./setup" |
| } |
| |
| variables { |
| input = "foo" |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "bad" |
| } |
| } |
| |
| run "site_one" { |
| // This run block can only be executed after the `primary_db` run block is completed, because it references the output of the `primary_db` run block. |
| state_key = "unique_2" |
| variables { |
| input = run.primary_db.value |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "site_two" { |
| // This run block can only be executed after the `primary_db` run block is completed, because it references the output of the `primary_db` run block. |
| // After that, it can be executed in parallel with the `site_one` run block. |
| state_key = "unique_3" |
| variables { |
| input = run.primary_db.value |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "using_external_db" { |
| // This run block does not reference the output of any other run block, and it has a different state key from its |
| // preceding runs, so it can be executed in parallel with runs `primary_db` and `secondary_db`. |
| state_key = "unique_4" |
| variables { |
| input = "externally_created_db" |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "site_four" { |
| // This run block has set `parallel = false`. |
| // Therefore, it will wait for all preceding runs to complete before it can be executed. |
| state_key = "unique_5" |
| |
| // This overrides the global parallel flag. |
| parallel = false |
| variables { |
| input = run.secondary_db.value |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "site_five" { |
| // This run block will wait for the run `site_four` to complete, because run `site_four` has the `parallel` attribute set to `false`. |
| state_key = "unique_6" |
| variables { |
| input = run.secondary_db.value |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "site_six" { |
| // This run block can only be executed after the run `site_five` is completed, because it references the output of the run `site_five`. |
| state_key = "unique_7" |
| variables { |
| input = run.site_five.value |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "same_state" { |
| // This run block uses an existing state key "unique_7", so it will wait for the run `site_six` to complete. |
| state_key = "unique_7" |
| variables { |
| input = "another_external_db" |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| |
| run "site_eight" { |
| // This run block is entirely unrelated to the other run blocks, however, because `site_four`, |
| // which is one of its preceding runs has the `parallel` attribute set to `false`, |
| // it cannot run until site_four is completed. It can be executed in parallel with `site_five` and `site_six`. |
| state_key = "unique_7" |
| variables { |
| input = "yet_another_external_db" |
| } |
| |
| assert { |
| condition = output.value == var.foo |
| error_message = "double bad" |
| } |
| } |
| ``` |
| |
| -> **Note:** When you configure a series of runs that have `parallel=true` in a test file and include a single run with `parallel=false` you create synchronization point that divides the workflow into two groups. All runs before the `parallel=false` run must complete first. Then, after the run with `parallel=false` completes, the subsequent runs begin. |
| |
| ``` |
| Run A (parallel: true) |
| Run B (parallel: true) |
| Run C (parallel: false) |
| Run D (parallel: true) |
| Run E (parallel: true) |
| ``` |
| |
| Runs A and B execute simultaneously. Run C waits until both A and B are done before starting. Finally, runs D and E run in parallel, but only after C completes. |