Skip to main content

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 files in the current directory or in a directory called tests. You can customize this behavior using the options below.

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.

Directory structure

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

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

Testing modules

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

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

The *.tftest.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 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:

NameTypeDescription
assertblockDefines 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.
moduleblockOverrides the module being tested. You can use this to load a helper module for more elaborate tests.
expect_failureslistA list of resources that should fail to provision in the current run.
variablesblockDefines variables for the current test case. See the variables section.
commandplan or applyDefines the command which OpenTofu will execute, plan or apply. Defaults to apply.
plan_optionsblockOptions for the plan or apply operation.
providersobjectAliases for providers.

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.

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 to load or any other module source.
  • The version specifies the version of the module you want to use.

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:

OrderSource
1Environment variables with the TF_VAR_ prefix.
2tfvar files specified: terraform.tfvars and *.auto.tfvars.
3Commandline variables defined using the flag -var, and the variables defined in the files specified by the flag -var-file.
4The variables from the variables block in a test file.
5The 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:

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

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:

Code Block
run "main" {
command = plan

variables {
instances = -1
}

expect_failures = [
var.instances,
]
}

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

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.

Code Block
run "test-failure" {
variables {
# This healthcheck endpoint won't exist:
health_endpoint = "/nonexistent"
}

expect_failures = [
# We expect this to fail:
check.health
]
}

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:

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

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

NameDescription
modeChange this option from normal (default) to refresh-only in order to only refresh the local state from the remote infrastructure.
refreshSet this option to false to disable checking for external changes in relation to the state file. Similar to tofu plan -refresh=false.
replaceForce replacing the specified list of resources, such as [docker_image.build] in the above example. Similar to tofu plan -replace=docker_image.build.
targetLimit planning to the specified list of modules or resources. Similar to tofu plan -target=docker_image.build.

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:

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

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.

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.