A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://developer.hashicorp.com/terraform/language/v1.6.x/tests below:

Tests - Configuration Language | Terraform

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.

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

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:

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.

# 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.

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

}

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 An optional variables block. module An optional module block. providers An optional providers attribute. assert 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 or an apply operation.

The plan_options block allows test authors to customize the planning mode and options they would typically need to edit via command-line flags and options. We cover the -var and -var-file options in the Variables section.

Assertions

Terraform run block assertions are Custom Conditions, consisting of a condition and an error message.

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 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, this is a valid condition: condition = output.bucket_name == "test_bucket".

You can provide values for Input 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 above:

# 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 and with Variable Definition Files.

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

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.

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

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 section.

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 as if Terraform were creating them within the main configuration. 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.

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 to allow tests, instead of the configuration, to specify the region. In this example, we are going to test the following configuration file:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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.

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, the module block within test files only supports the source attribute and the 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 and 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.

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.

# 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.

# 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.

# 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.

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

Note: The loader module only uses data sources for resources created by the setup module, and does not load data sources based on the main configuration.

The state for the main configuration is destroyed first when Terraform performs the cleanup operations after the test has completed. Any data sources in alternate modules based on the main configuration will likely error during the cleanup operation as they will attempt to be refreshed, and will return unavailable or not found errors as the underlying infrastructure has already been destroyed.

The order of the destroy operations changed starting with the Terraform v1.7 series and tests written for future versions do not need this consideration.

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

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 example, we 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 the following order, and this order is important because it affects the structure of your testing files:

  1. Resources in the main state file. Do not create resources in alternate modules that depend on resources from your main configuration.
  2. Resources created by alternate modules in reverse run block order.

From our example, any resources created in the "verify" run block would be destroyed before resources created in the "setup" run block. Note, that in our example this doesn't particularly matter as our "verify" run block only loads a data source and creates no resources.

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.

By default, if any 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.

# 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.

# 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.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4