- Basic CLI Features
- Command: test
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.
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
resource "local_file" "test" {
filename = "${path.module}/test.txt"
content = "Hello world!"
}
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
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.
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 runtofu 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 loadsterraform.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:
- 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.
.
├── 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.
.
├── 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.
.
├── 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.
.
├── module1
│ ├── main.tf
│ ├── foo.tf
│ ├── bar.tf
│ └── tests
│ ├── main.tftest.hcl
│ ├── foo.tftest.hcl
│ └── bar.tftest.hcl
└── module2
└── ...
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
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 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. |
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:
- The
condition
is an OpenTofu condition which should returntrue
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. - The
error_message
is a string explaining what happened when the test fails.
As a simple example, you can write an assert
block as follows:
- main.tftest.hcl
- main.tf
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
run "test" {
assert {
condition = file(local_file.test.filename) == "Hello world!"
error_message = "Incorrect content in ${local_file.test.filename}."
}
}
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:
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.
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.
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
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
}
}
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
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}"
}
}
# 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: terraform.tfvars and *.auto.tfvars . |
3 | Commandline variables defined using the flag -var , and the variables defined in the files specified by the flag -var-file . |
4 | The variables from the variables block in a test file. |
5 | 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
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
# 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}"
}
}
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
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
run "main" {
command = plan
variables {
instances = -1
}
expect_failures = [
var.instances,
]
}
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.
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
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
run "test-failure" {
variables {
# This healthcheck endpoint won't exist:
health_endpoint = "/nonexistent"
}
expect_failures = [
# We expect this to fail:
check.health
]
}
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
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
run "test" {
command = plan
plan_options {
refresh = false
}
variables {
image_name = "myapp"
}
assert {
condition = docker_image.build.name == "myapp"
error_message = "Missing build resource"
}
}
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 . |
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.
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
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
// 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}"
}
}
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
# Copyright (c) The OpenTofu Authors
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2023 HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
# 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.
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "3.0.2"
}
}
}
resource "docker_image" "build" {
name = "myapp"
build {
context = "."
}
}