Spec-Zone.ru › OpenTofu 1.9

Command: test

The tofu test command lets you test your OpenTofu configuration by creating real infrastructure and checking that the required conditions (assertions) are met. Once the test is complete, OpenTofu destroys the resources it created.

Usage​

Usage: tofu test [options].

This command will execute all *.tftest.hcl, *.tftest.json, *.tofutest.hcl, and *.tofutest.json files in the current directory or in a directory called tests. You can customize this behavior using the options below.

Example

Consider the following simple example which creates a test.txt file from main.tf and then checks that the main code has successfully performed its job from main.tftest.hcl.

  • main.tf
  • main.tftest.hcl
Code Block
resource "local_file" "test" {
  filename = "${path.module}/test.txt"
  content  = "Hello world!"
}
Code Block
run "test" {
  assert {
    condition     = file(local_file.test.filename) == "Hello world!"
    error_message = "Incorrect content in ${local_file.test.filename}."
  }
}

You can run tofu init followed by tofu test to execute the test which will apply the main.tf file and test it against the assertion in main.tftest.hcl. This is just a simple illustration. You can find more comprehensive examples below.

Extension Precedence​

When both .tftest.hcl and .tofutest.hcl files with the same base name are present in a directory, OpenTofu will prioritize the .tofutest.hcl file and ignore the .tftest.hcl file. For example:

  • If both main.tftest.hcl and main.tofutest.hcl exist in the same directory, OpenTofu will only load main.tofutest.hcl and ignore main.tftest.hcl.

This ensures that .tofu files always take precedence over .tf files when both are available. This scenario can be useful for module authors who want their modules to support both OpenTofu and Terraform and want to create different tests for each.

The same rule applies to JSON-based test files:

  • if both main.tftest.json and main.tofutest.json exist in the same directory, OpenTofu will only load main.tofutest.json and ignore main.tftest.json.

Options​

  • -test-directory=path Set the test directory (default: "tests"). OpenTofu will search for test files in the specified directory and also the current directory when you run tofu test. The path should be relative to the current working directory.
  • -filter=testfile Specify an individual test file to run. Use this option multiple times to specify more than one file. The path should be relative to the current working directory.
  • -var 'foo=bar' Set an input variable of the root module. Specify this option multiple times to add more than one variable.
  • -var-file=filename Set multiple variables from the specified file. In addition to this file, OpenTofu automatically loads terraform.tfvars and *.auto.tfvars. Use this option multiple times to specify more than one file.
  • -json Change the output format to JSON.
  • -no-color Disable colorized output in the command output.
  • -verbose Print the plan or state for each test run block as it executes.
Note

Use of variables in module sources, backend configuration, or encryption block requires assigning values to root module variables when running tofu test.

Directory structure​

The tofu test command supports two directory layouts, flat or nested:

  • Flat layout
  • Nested layout

This layout places the *.tftest.hcl test files directly next to the *.tf files they test. There are no rules that each *.tf file must have its own test file, but it is a good practice to follow.

Code Block
.
├── main.tf
├── main.tftest.hcl
├── foo.tf
├── foo.tftest.hcl
├── bar.tf
└── bar.tftest.hcl

This layout places the *.tftest.hcl files in a separate tests directory. Similar to the flat layout, there are no rules that each *.tf file must have its own test file, but it is a good practice to follow.

Code Block
.
├── main.tf
├── foo.tf
├── bar.tf
└── tests
     ├── main.tftest.hcl
     ├── foo.tftest.hcl
     └── bar.tftest.hcl

Testing modules​

When testing modules, you can use one of the above directory structures for each module:

  • Flat layout
  • Nested layout

With this layout, run tofu test -test-directory=./path/to/module to test the module in question.

Code Block
.
├── module1
│    ├── main.tf
│    ├── main.tftest.hcl
│    ├── foo.tf
│    ├── foo.tftest.hcl
│    ├── bar.tf
│    └── bar.tftest.hcl
└── module2
     └── ...

With this layout, change your working directory to your module path and run tofu test to test the module in question.

Code Block
.
├── module1
│    ├── main.tf
│    ├── foo.tf
│    ├── bar.tf
│    └── tests
│         ├── main.tftest.hcl
│         ├── foo.tftest.hcl
│         └── bar.tftest.hcl
└── module2
     └── ...
Hint

You can use the -filter=sometest.tftest.hcl option to run a limited set of test files. Use the option multiple times to run more than one test file.

The *.tftest.hcl / *.tofutest.hcl file structure​

The testing language of OpenTofu is similar to the main OpenTofu language and uses the same block structure.

A test file consists of:

  • The run blocks: define your tests.
  • A variables block (optional): define variables for all tests in the current file.
  • The provider blocks (optional): define the providers to be used for the tests.
  • The mock_provider blocks (optional): define the providers to be mocked.
  • The override_resource blocks (optional): define the resources to be overridden.
  • The override_data blocks (optional): define the data sources to be overridden.
  • The override_module blocks (optional): define the module calls to be overridden.

The run block​

A run block contains a single test case which runs either tofu apply or tofu plan and then evaluates all assert blocks. Once the test is complete, it uses tofu destroy to remove the temporarily created resources.

A run block consists of the following elements:

Name Type Description
assert block Defines assertions that check if your code (e.g. main.tf) created the infrastructure correctly. If you do not specify any assert blocks, OpenTofu simply applies the configuration without any assertions.
module block Overrides the module being tested. You can use this to load a helper module for more elaborate tests.
expect_failures list A list of resources that should fail to provision in the current run.
variables block Defines variables for the current test case. See the variables section.
command plan or apply Defines the command which OpenTofu will execute, plan or apply. Defaults to apply.
plan_options block Options for the plan or apply operation.
providers object Aliases for providers.
override_resource block Defines a resource to be overridden for the run.
override_data block Defines a data source to be overridden for the run.
override_module block Defines a module call to be overridden for the run.

The run.assert block​

You can specify assert blocks inside your run block to test the state of your infrastructure after the apply or plan operation is complete. There is no theoretical limit to the number of blocks you can define.

Each block requires the following two attributes:

  1. The condition is an OpenTofu condition which should return true for the test to pass, false for the test to fail. The condition must reference a resource, data source, variable, output or module from the main code, otherwise OpenTofu will refuse to run the test.
  2. The error_message is a string explaining what happened when the test fails.
Example

As a simple example, you can write an assert block as follows:

  • main.tftest.hcl
  • main.tf
Code Block
run "test" {
  assert {
    condition     = file(local_file.test.filename) == "Hello world!"
    error_message = "Incorrect content in ${local_file.test.filename}."
  }
}
Code Block
resource "local_file" "test" {
  filename = "${path.module}/test.txt"
  content  = "Hello world!"
}

Please note that conditions only let you perform basic checks on the current OpenTofu state and use OpenTofu functions. You cannot define additional data sources directly in your test code. To work around this limitation, you can use the module block in order to load a helper module.

The run.module block​

In some cases you may find that the tools provided in the condition expression are not enough to test if your code created the infrastructure correctly.

You can use the module block to override the main module tofu test loads. This gives you the opportunity to create additional resources or data sources that you can use in your assert conditions.

Its syntax is similar to loading modules in normal OpenTofu code:

Code Block
run "test" {
  module {
    source = "./some-module"
  }
}

The module block has the following two attributes:

  • The source attribute points to the directory of the module or to the module registry. Remote sources are not allowed at this point.
  • The version specifies the version of the module you want to use.
Note

You cannot pass parameters directly in the module block as you may be used to from the normal OpenTofu code. Instead, you should use the variables block to pass parameters to your module.

Example

In this example project the main.tf file creates a Docker container with an nginx image exposed on port 8080. The main.tftest.hcl file needs to test if the webserver actually starts properly, but it cannot do that without a helper module.

To create the http data source, the main.tftest.hcl file loads the test-harness module. The test helper then loads the main module and adds the data source to check the HTTP response. Note that the data source in the test-harness has an explicit dependency on module.main to make sure that the data source only returns once the main module has finished its work.

  • main.tf
  • main.tftest.hcl
  • test-harness/helper.tf
Code Block
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "3.0.2"
    }
  }
}

resource "docker_container" "webserver" {
  name  = "nginx-test"
  image = "nginx"
  ports {
    internal = 80
    external = 8080
  }
}
Code Block
run "http" {
  # Load the test helper instead of the main module:
  module {
    source = "./test-harness"
  }

  # Check if the webserver returned an HTTP 200 status code:
  assert {
    condition     = data.http.test.status_code == 200
    error_message = "Incorrect status code returned: ${data.http.test.status_code}"
  }
}
Code Block
# Load the main module:
module "main" {
  source = "../"
}

# Fetch the website so the assert can do its job:
data "http" "test" {
  url = "http://localhost:8080"

  # Important! Wait for the main module to finish:
  depends_on = [module.main]
}

This project uses a third-party provider to launch the container. You can run it locally if you have a Docker Engine installed.

The variables and run.variables blocks​

The code under test (e.g. main.tf) will often have variable blocks that you need to fill from your test case. You can provide variables to your test run using any of the following methods:

Order Source
1 Environment variables with the TF_VAR_ prefix.
2 tfvar files specified in the current directory: terraform.tfvars and *.auto.tfvars.
3 tfvar files specified in the tests directory: tests/terraform.tfvars and tests/*.auto.tfvars.
4 Commandline variables defined using the flag -var, and the variables defined in the files specified by the flag -var-file.
5 The variables from the variables block in a test file.
6 The variables from the variables block in run block.

OpenTofu evaluates the variables in the order listed above, so you can use it to override the previously set variable. For example:

  • main.tftest.hcl
  • main.tf
Code Block
# First, set the variable here:
variables {
  name = "OpenTofu"
}

run "basic" {
  assert {
    condition     = output.greeting == "Hello OpenTofu!"
    error_message = "Incorrect greeting: ${output.greeting}"
  }
}

run "override" {
  # Override it for this test case only here:
  variables {
    name = "OpenTofu user"
  }
  assert {
    condition     = output.greeting == "Hello OpenTofu user!"
    error_message = "Incorrect greeting: ${output.greeting}"
  }
}
Code Block
variable "name" {}

output "greeting" {
  value = "Hello ${var.name}!"
}

The run.expect_failures list​

In some cases you may want to test deliberate failures of your code, for example to ensure your validation is working.

You can use the expect_failures inside a run block to specify which variables or resources should fail when the code is run with the given parameters.

For example, the test case below checks if the instances variable correctly fails validation when supplied with a negative number:

  • main.tftest.hcl
  • main.tf
Code Block
run "main" {
  command = plan

  variables {
    instances = -1
  }

  expect_failures = [
    var.instances,
  ]
}
Code Block
variable "instances" {
  type = number

  validation {
    condition     = var.instances >= 0
    error_message = "The number of instances must be positive or zero"
  }
}

You can also use the expect_failure clause to check lifecycle events like pre- or postconditions as well as the results of checks.

Limitation

The expect_failure list currently does not support testing resource creation failures. You must provide a lifecycle event in order to use expect_failure.

The example below checks if the misconfigured healthcheck fails. This ensures that the health check does not always return, even when it is running against the wrong endpoint.

  • main.tftest.hcl
  • main.tf
Code Block
run "test-failure" {
  variables {
    # This healthcheck endpoint won't exist:
    health_endpoint = "/nonexistent"
  }

  expect_failures = [
    # We expect this to fail:
    check.health
  ]
}
Code Block
variable "health_endpoint" {
  default = "/"
}

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "3.0.2"
    }
  }
}

resource "docker_container" "webserver" {
  name  = ""
  image = "nginx"
  rm    = true

  ports {
    internal = 80
    external = 8080
  }
}

check "health" {
  data "http" "www" {
    url = "http://localhost:8080${var.health_endpoint}"
    depends_on = [docker_container.webserver]
  }

  assert {
    condition = data.http.www.status_code == 200
    error_message = "Invalid status code returned: ${data.http.www.status_code}"
  }
}

The run.command setting and the run.plan_options block​

By default, tofu test uses tofu apply to create real infrastructure. In some cases, for example if the real infrastructure is very expensive or impossible to run for testing purposes, it can be useful to only run tofu plan instead. You can use the command = plan setting to perform a plan instead of an apply. The following example tests if the variable is correctly passed to the docker_image resource without actually applying the plan:

  • main.tftest.hcl
  • main.tf
Code Block
run "test" {
  command = plan
  plan_options {
    refresh = false
  }
  variables {
    image_name = "myapp"
  }
  assert {
    condition     = docker_image.build.name == "myapp"
    error_message = "Missing build resource"
  }
}
Code Block
variable "image_name" {
  default = "app"
}

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "3.0.2"
    }
  }
}

resource "docker_image" "build" {
  name = var.image_name
  build {
    context = "."
  }
}

Regardless of the command setting, you can use the plan_options block to specify the following additional options for both modes:

Name Description
mode Change this option from normal (default) to refresh-only in order to only refresh the local state from the remote infrastructure.
refresh Set this option to false to disable checking for external changes in relation to the state file. Similar to tofu plan -refresh=false.
replace Force replacing the specified list of resources, such as [docker_image.build] in the above example. Similar to tofu plan -replace=docker_image.build.
target Limit planning to the specified list of modules or resources. Similar to tofu plan -target=docker_image.build.
Tip

You can use these options in conjunction with provider overrides to create fully offline tests. See the Providers section below for an example.

The providers block​

In some cases you may want to override provider settings for test runs. You can use the provider blocks outside of run block to provide additional configuration options for providers, such as credentials for a test account.

Code Block
provider "aws" {
  // Add additional settings here
}

This feature can also enable partially or fully offline tests if the provider supports it. The following example illustrates a fully offline test with the AWS provider and an S3 bucket resource:

  • main.tftest.hcl
  • main.tf
Code Block
// Configure the AWS provider to run fake credentials and without
// any validations. Not all providers support this, but when they
// do, you can run fully offline tests.
provider "aws" {
  access_key = "foo"
  secret_key = "bar"

  skip_credentials_validation = true
  skip_region_validation      = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
}

run "test" {
  // Run in plan mode to skip applying:
  command = plan

  // Disable the refresh to prevent reaching out to the AWS API:
  plan_options {
    refresh = false
  }

  // Test if the bucket name is correctly passed to the aws_s3_bucket
  // resource:
  variables {
    bucket_name = "test"
  }
  assert {
    condition     = aws_s3_bucket.test.bucket == "test"
    error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
  }
}
Code Block
variable "bucket_name" {}

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

resource "aws_s3_bucket" "test" {
  bucket = var.bucket_name
}

Provider aliases​

In addition to provider overrides, you can alias providers in order to replace them with a different provider inside your run block. This is useful when you want to have two provider configurations within the same test file and switch between them.

In the example below, the sockettest test case loads a different Docker provider configuration than the rest of the file.

  • main.tftest.hcl
  • main.tf
Code Block
# This is the default "docker" provider for this file:
provider "docker" {
  host = "tcp://0.0.0.0:2376"
}

# This will be the override:
provider "docker" {
  alias = "unixsocket"
  host = "unix:///var/run/docker.sock"
}

run "sockettest" {
  # Replace the "docker" provider for this test case only:
  providers = {
    docker = docker.unixsocket
  }

  assert {
    condition     = docker_image.build.name == "myapp"
    error_message = "Missing build resource"
  }
}

// Add other tests with the original provider here.
Code Block
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "3.0.2"
    }
  }
}

resource "docker_image" "build" {
  name = "myapp"
  build {
    context = "."
  }
}

The mock_provider blocks​

A mock_provider block allows you to replace provider configuration by a mocked one. In such scenario, creation and retrieval of provider resources and data sources will be skipped. Instead, OpenTofu will automatically generate all computed attributes and blocks to be used in tests.

Note

Learn more on how OpenTofu produces automatically generated values.

Mock providers also support alias field as well as mock_resource and mock_data blocks. In some cases, you may want to use default values instead of automatically generated ones by passing them inside defaults field of mock_resource or mock_data blocks.

Additionally, you can use override_resource and override_data blocks to override resources or data sources in the scope of a single provider. Read more about overriding in the next section.

In the example below, we test if the bucket name is correctly passed to the resource without actually creating it:

  • main.tftest.hcl
  • main.tf
Code Block
// All resources and data sources provided by `aws.mock` provider
// will be mocked. Their values will be automatically generated.
mock_provider "aws" {
  alias = "mock"
}

// The same goes for `local` provider. Also, every `local_file`
// data source will have its `content` set to `test`.
mock_provider "local" {
  mock_data "local_file" {
    defaults = {
      content = "test"
    }
  }
}

// Test if the bucket name is correctly passed to the aws_s3_bucket
// resource from the local file.
run "test" {
  // Use `aws.mock` provider for this test run only.
  providers = {
    aws = aws.mock
  }

  assert {
    condition     = aws_s3_bucket.test.bucket == "test"
    error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
  }
}
Code Block
data "local_file" "bucket_name" {
  filename = "bucket_name.txt"
}

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

resource "aws_s3_bucket" "test" {
  bucket = data.local_file.bucket_name.content
}

The override_resource and override_data blocks​

In some cases you may want to test your infrastructure with certain resources or data sources being overridden. You can use the override_resource or override_data blocks to skip creation and retrieval of these resources or data sources using the real provider. Instead, OpenTofu will automatically generate all computed attributes and blocks to be used in tests.

Note

Learn more on how OpenTofu produces automatically generated values.

These blocks consist of the following elements:

Name Type Description
target reference Required. Address of the target resource or data source to be overridden.
values object Custom values for computed attributes and blocks to be used instead of automatically generated.

You can use override_resource or override_data blocks for the whole test file or inside a single run block. The latter takes precedence if both specified for the same target.

In the example below, we test if the bucket name is correctly passed to the resource without actually creating it:

  • main.tftest.hcl
  • main.tf
Code Block
// This data source will not be called for any run
// in this `.tftest.hcl` file. Instead, `values` object
// will be used to populate `content` attribute. Other
// attributes and blocks will be automatically generated.
override_data {
  target = data.local_file.bucket_name
  values = {
    content = "test"
  }
}

// Test if the bucket name is correctly passed to the aws_s3_bucket
// resource from the local file.
run "test" {
  // S3 bucket will not be created in AWS for this run,
  // but it's available to use in both tests and configuration.
  override_resource {
    target = aws_s3_bucket.test
  }

  assert {
    condition     = aws_s3_bucket.test.bucket == "test"
    error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
  }
}
Code Block
data "local_file" "bucket_name" {
  filename = "bucket_name.txt"
}

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

resource "aws_s3_bucket" "test" {
  bucket = data.local_file.bucket_name.content
}
Limitation

You cannot use override_resource or override_data with a single instance of a resource or data source. Each instance of a resource or data source must be overridden.

Automatically generated values​

Mocking resources and data sources requires OpenTofu to automatically generate computed attributes without calling respective providers. When generating these values, OpenTofu cannot follow custom provider logic, so it uses simple rules based on value type:

Attribute type Generated value
number 0
bool false
string A random alpha-numeric string.
list An empty list.
map An empty map.
set An empty set.
object An object with its fields populated by the same logic recursively.
tuple An empty tuple.
Note

You can set custom values to use instead of automatically generated ones via respective mock or override fields. Keep in mind, it's only possible for computed attributes and configuration values cannot be changed.

The override_module block​

In some cases you may want to test your infrastructure with certain module calls being overridden. You can use the override_module block to ignore all the configuration provided by called module. In this case, OpenTofu will use custom values specified in the override_module block as module outputs.

The block consist of the following elements:

Name Type Description
target reference Required. Address of the target module call to be overridden.
outputs object Values to be used as module call outputs. If an output is not specified, OpenTofu will set it to null by default.

You can use override_module block for the whole test file or inside a single run block. The latter takes precedence if both specified for the same target.

In the example below, we test if the bucket name is correctly passed from the module without actually calling it:

  • main.tftest.hcl
  • main.tf
  • bucket_meta/main.tf
Code Block
// All the module configuration will be ignored for this
// module call. Instead, the `outputs` object will be used
// to populate module outputs.
override_module {
  target = module.bucket_meta
  outputs = {
    name = "test"
    tags = {
      Environment = "Test Env"
    }
  }
}

// Test if the bucket name is correctly passed to the aws_s3_bucket
// resource from the module call.
run "test" {
  // S3 bucket will not be created in AWS for this run,
  // but it's available to use in both tests and configuration.
  override_resource {
    target = aws_s3_bucket.test
  }

  assert {
    condition     = aws_s3_bucket.test.bucket == "test"
    error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
  }

  assert {
    condition     = aws_s3_bucket.test.tags["Environment"] == "Test Env"
    error_message = "Incorrect `Environment` tag: ${aws_s3_bucket.test.tags["Environment"]}"
  }
}
Code Block
module "bucket_meta" {
  source = "./bucket_meta"
}

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

resource "aws_s3_bucket" "test" {
  bucket = module.bucket_meta.name
  tags   = module.bucket_meta.tags
}
Code Block
data "local_file" "bucket_name" {
  filename = "bucket_name.txt"
}

output "name" {
  value = data.local_file.bucket_name.content
}

output "tags" {
  value = {
    Environment = "Dev"
  }
}
Limitation

You cannot use override_module with a single instance of a module call. Each instance of a module call must be overridden.

Copyright (c) The OpenTofu Authors
Copyright (c) 2014 HashiCorp, Inc.
Mozilla Public License, version 2.0
https://opentofu.org/docs/v1.9/cli/commands/test/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API