Fixes #292: Testing feature documentation (#939)

Signed-off-by: Janos Bonic <86970079+janosdebugs@users.noreply.github.com>
Co-authored-by: James Humphries <James@james-humphries.co.uk>
This commit is contained in:
Janos 2023-12-01 09:34:59 +01:00 committed by GitHub
parent aabe1c95be
commit e2613d7bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 742 additions and 301 deletions

View File

@ -1,301 +0,0 @@
---
description: >-
The tofu test command performs integration tests of OpenTofu modules.
---
# Command: test
The `tofu test` command runs integration tests to ensure intended behavior of your OpenTofu configuration. It creates real
infrastructure, asserts conditional checks against the attributes of provisioned resources and attempts
to clean up the testing infrastructure upon completion.
## Usage
Usage: `tofu test [options]`.
The `test` command executes automated integration tests following the next steps:
1. [Define](#how-to-define-a-test-suite) a test `suite` by reading [test files](#how-to-define-a-test-file) `*.tftest.hcl`.
2. Read all global input [variables](#variables) for the entire test `suite`.
3. Establish execution order by sorting test files alphabetically.
4. Sequentially execute all test files in the `suite`:
1. Sequentially execute all test [`runs`](#run-block) defined in a single test file:
1. Prepare and validate the `run` configuration;
2. Perform `tofu plan+apply`, or `tofu apply`;
3. Check for expected failures;
4. Verify assertion conditions;
5. Update the `suite` _status_.
2. Printout the summary for the entire test file.
3. Printout the summary for every `run`.
4. Sequentially destroy the resources provisioned by every `run` in a single test file in the reverse order,
starting from the last executed `run`.
5. Printout an overall summary of the test `suite` execution.
6. Terminate with the status code `0` if all tests passed; `1` otherwise.
:::info
Monitor the test output for successful cleanup because the execution will continue even if some destroy operations fail.
:::
## General Options
* `-test-directory=path` Set the test directory (default: "tests"). OpenTofu will search for test files in the set directory
as well as in the directly in which `tofu test` was executed.
:::warning
The path should be relative to the directory in which `tofu test` is called.
:::
* `-filter=testfile` Specify an individual test file to use for testing.
The option can be used multiple times to specify several files.
:::warning
The path should be relative to the directory in which `tofu test` is called.
:::
* `-var 'foo=bar'` Set the input variables of the root module.
The option can be used multiple times to set several variables.
* `-var-file=filename` Set the path to the file with variables values in addition to the default
files terraform.tfvars and *.auto.tfvars. The option can be used multiple times to specify several files.
* `-json` Set for test outputs to be formatted as JSON.
* `-no-color` Disable color codes in the command output.
* `-verbose` Print the plan or state for each test run block as it executes.
## How to define a test suite
In this subsection we will illustrate how to filter test files when defining a test `suite`.
### Flat structure
Assuming the OpenTofu module structure below
```commandline
.
├── LICENSE
├── README.md
├── main.tf
├── main.tftest.hcl
├── foo.tf
├── foo.tftest.hcl
├── bar.tf
├── bar.tftest.hcl
├── outputs.tf
└── variables.tf
```
- When you execute `tofu test` in the project directory, the test `suite` will consist of the files:
`bar.tftest.hcl`, `foo.tftest.hcl`, `main.tftest.hcl`.
- To select some specific files from the `suite`, use the `-filter` option.
For example, when you execute `tofu test -filter=foo.tftest.hcl -filter=bar.tftest.hcl` in the project directory,
the files `foo.tftest.hcl` and `bar.tftest.hcl` will be selected only.
### Nested structure
Assuming the OpenTofu module structure below
```commandline
.
├── LICENSE
├── README.md
├── main.tf
├── main.tftest.hcl
├── outputs.tf
├── variables.tf
├── tests
│   └── foo.tftest.hcl
└── submodule
    ├── foo.tf
    ├── foo.tftest.hcl
    ├── main.tf
    ├── main.tftest.hcl
    ├── outputs.tf
    ├── variables.tf
    └── tests
        └── foo.tftest.hcl
```
- When you execute `tofu test` in the project directory, the test `suite` will consist of the files: `main.tftest.hcl`
and `tests/foo.tftest.hcl`.
- To specify the directory other that the default `tests`, use the `-test-directory` option.
For example, when you execute `tofu test -test-directory=submodule` in the project directory,
the test `suite` will consist of the files: `main.tftest.hcl`, `submodule/foo.tftest.hcl` and `submodule/main.tftest.hcl`.
- You can combine the options `-test-directory` and `-filter` to select specific test files.
For example, when you execute `tofu test -test-directory=submodule -filter=submodule/foo.tftest.hcl` in the project directory,
the file `submodule/foo.tftest.hcl` will be selected only.
:::warning
Make sure that the path is specified relative to the directory in which `tofu test` is called.
For example, when you execute `tofu test -test-directory=submodule -filter=foo.tftest.hcl` in the project directory,
no test files will be found.
:::
## How to define a test file
Tests are defined using three kinds of blocks in the way that every test file
- _shall_ contain _at least one_ `run` block to define a single test case;
- _may_ contain _at most one_ `variables` block to define "global" variables shared by all `runs` defined in a single file;
- _may_ contain _multiple_ `provider` blocks to configure providers shared by all `runs` in a single file.
:::info
Note that if a test file contains several `run` blocks, OpenTofu will execute them sequentially from the top to
the bottom of the file.
:::
### `run` block
<!-- details: /internal/configs/test_file.go:239 decodeTestRunBlock -->
The `run` block defines a _single test_. It specifies the operation which OpenTofu will execute,
sets mocks for providers,
and defines the checks to assess the state after the OpenTofu operation was executed.
The following non-mutually exclusive blocks and attributes can be defined within the scope of the `run` block to configure the test.
| Name | Type | Multiple Allowed | Description |
|:------------------|:-------|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------------|
| `assert` | block | true | Defines the state assessment criteria and the assertion message. |
| `expect_failures` | list | false | Defines the check for "unhappy path" by listing resources and values which are expected to fail as the result of OpenTofu operation. |
| `variables` | block | false | Defines "local" variables as [key-value pairs](/docs/language/expressions/types/#mapsobjects). |
| `command` | string | false | Defines the command which OpenTofu will execute, _plan_ or _apply_. Defaults to _apply_. |
| `plan_options` | block | false | Defines options for the _plan_ operation. |
| `module` | block | false | Defines module used at execution of the OpenTofu operation. |
| `providers` | object | false | Defines aliases to mock providers. |
Find below detailed description of every listed attribute.
### `run.assert`
The `assert` block is the key configuration which defines how to assess OpenTofu configuration behavior against
a reference using two required attributes:
- `condition`: check [condition](https://opentofu.org/docs/language/expressions/custom-conditions#condition-expressions)
as a boolean expression.
- `error_message`: assertion message which will be printed if the test fails.
```hcl
run "test" {
variables {
want_attr0 = "test-value"
want_attr1 = 1
}
assert {
condition = resource_type.resource_name.attr0 == var.want_attr0
error_message = "failed validation of attr0"
}
assert {
condition = resource_type.resource_name.attr1 == var.want_attr1
error_message = "failed validation of attr1"
}
}
```
:::info
If several assertions are present in a single `run`, they will be run sequentially, and if some of them fail,
the total numer of failed tests will be reported as 1.
The opposite is also true, 1 successful test will be reported if all checks are successful.
:::
### `run.expect_failures`
The `expect_failures` attribute lists the resources, or variable which are expected to fail during the test.
```hcl
run "unhappy-path" {
variable {
input0 = "faulty-input"
}
expect_failures = [
# we expect the module to validate the input,
# and the validation to fail given the wrong input value defined in the variables block
input.input0,
]
}
```
### `run.plan_options`
The `plan_options` block can be used to defined how OpenTofu will execute the `plan` command during the test.
It can be configured by the following optional attributes.
| Name | Type | Default | Description |
|:--------|:-------|:---------|:-----------------------------------------------------------------------|
| mode | string | "normal" | The planning mode to run. Allowed values: _normal_ or _refresh-only_. |
| refresh | bool | true | Analog to `tofu plan -refresh`. |
| replace | list | null | Analog to `tofu plan -refresh=ADDRESS`. |
| target | list | null | Analog to `tofu plan -target=ADDRESS`. |
:::warning
If the `mode` is set to _refresh-only_, the `refresh` parameter cannot be set to _false_.
:::
### `run.module`
The `module` block defines a module used during the execution of the OpenTofu operation within the scope of the test.
It is useful for mocking providers required by the tested OpenTofu configuration.
The `module` block is defined by two attributes:
- `source` (required): module [source](/docs/language/modules/sources).
- `version` (optional): module version.
```hcl
run "setup-module" {
module {
source = "./modules/my_module"
}
}
```
:::info
If a module is specified in the `run` block, `tofu init` execution will be required before running `tofu test`.
:::
### `run.providers`
The `providers` object allows to mock providers for execution of the `run`.
### `provider` block
<!-- details: /internal/command/testing/test_provider.go -->
The `provier` block defines a single provider shared by all `runs` defined in a single test file.
It is defined using the following attributes:
| Name | Type | Description |
|:----------------|:-------|:-----------------------------------------------------------------------------------------------------|
| alias | string | The provider's alies within the scope of a given test file. |
| resource_prefix | string | The OpenTofu configuration tag word used to recognize a [resource](docs/language/resources/syntax/). |
| data_prefix | string | The OpenTofu configuration tag word used to recognize a [data source](/docs/language/data-sources/). |
## Variables
Tests can be defined using variables specified in several ways.
The variables will be loaded in the following order, with later sources taking precedence over earlier ones:
| Order | Source | Scope |
|:------:|:-------------------------------------------------------------------------------------------------------------------------------|:-----------------|
| 1 | Environment variables with the prefix `TF_VAR_`; | All test files |
| 2 | tfvar files specified: `terraform.tfvars` and `*.auto.tfvars`; | All test files |
| 3 | Commandline variables defined using the flag `-var`, and the variables defined in the files specified by the flag `-var-file`; | All test files |
| 4 | The variables from the `variables` block in a test file. | Single test file |
| 5 | The variables from the `variables` block in `run` block. | Single test |
:::info
The variable value defined by the flags `-var` and `-var-file` will depend on their order: the latest found value will be used.
For example, **given** that the file `test.tfvars` contains the value `my_var="bar"`,
**when** `tofu test -var=my_var="foo" -var-file=test.tfvars -var=my_var="qux"` is executed,
**then** the variable `my_var` will be assigned the value `"qux"`.
:::

View File

@ -0,0 +1,4 @@
.terraform
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup

View File

@ -0,0 +1,35 @@
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}"
}
}

View File

@ -0,0 +1,11 @@
run "test-failure" {
variables {
# This healthcheck endpoint won't exist:
health_endpoint = "/nonexistent"
}
expect_failures = [
# We expect this to fail:
check.health
]
}

View File

@ -0,0 +1,8 @@
variable "instances" {
type = number
validation {
condition = var.instances >= 0
error_message = "The number of instances must be positive or zero"
}
}

View File

@ -0,0 +1,11 @@
run "main" {
command = plan
variables {
instances = -1
}
expect_failures = [
var.instances,
]
}

View File

@ -0,0 +1,17 @@
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
}
}

View File

@ -0,0 +1,12 @@
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}"
}
}

View File

@ -0,0 +1,12 @@
# 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]
}

View File

@ -0,0 +1,4 @@
module "greet" {
source = "./module"
name = "greet"
}

View File

@ -0,0 +1,3 @@
run "test" {
}

View File

@ -0,0 +1,5 @@
variable "name" {}
output "greeting" {
value = "Hello ${var.name}!"
}

View File

@ -0,0 +1,12 @@
run "test" {
module {
source="../"
}
variables {
name = "OpenTofu"
}
assert {
condition = output.greeting == "Hello OpenTofu!"
error_message = "Incorrect greeting: ${output.greeting}"
}
}

View File

@ -0,0 +1,9 @@
variable "bucket_name" {}
provider "aws" {
region = "us-east-2"
}
resource "aws_s3_bucket" "test" {
bucket = var.bucket_name
}

View File

@ -0,0 +1,32 @@
// 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}"
}
}

View File

@ -0,0 +1,3 @@
FROM nginx
# It doesn't really matter what we do here, it won't run anyway.

View File

@ -0,0 +1,19 @@
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 = "."
}
}

View File

@ -0,0 +1,13 @@
run "test" {
command = plan
plan_options {
refresh = false
}
variables {
image_name = "myapp"
}
assert {
condition = docker_image.build.name == "myapp"
error_message = "Missing build resource"
}
}

View File

@ -0,0 +1 @@
FROM nginx

View File

@ -0,0 +1,15 @@
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "3.0.2"
}
}
}
resource "docker_image" "build" {
name = "myapp"
build {
context = "."
}
}

View File

@ -0,0 +1,24 @@
# 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.

View File

@ -0,0 +1,4 @@
resource "local_file" "test" {
filename = "${path.module}/test.txt"
content = "Hello world!"
}

View File

@ -0,0 +1,6 @@
run "test" {
assert {
condition = file(local_file.test.filename) == "Hello world!"
error_message = "Incorrect content in ${local_file.test.filename}."
}
}

View File

@ -0,0 +1,44 @@
#!/bin/bash
tofu version 2>/dev/null >/dev/null
if [ $? -ne 0 ]; then
set -e
TOFU_VERSION="1.6.0-alpha2"
OS="$(uname | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m | sed -e 's/aarch64/arm64/' -e 's/x86_64/amd64/')"
TEMPDIR="$(mktemp -d)"
pushd "${TEMPDIR}" >/dev/null
wget "https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_${OS}_${ARCH}.zip"
unzip "tofu_${TOFU_VERSION}_${OS}_${ARCH}.zip"
sudo mv tofu /usr/local/bin/tofu
popd >/dev/null
rm -rf "${TEMPDIR}"
set +e
fi
ERROR=0
for testcase in $(ls -d */); do
testcase=$(echo -n "${testcase}" | sed -e 's$/$$')
(
cd $testcase
tofu init
RESULT=$?
if [ "$RESULT" -ne 0 ]; then
exit "$RESULT"
fi
tofu test
exit $?
) 2>/tmp/${testcase}.log >/tmp/${testcase}.log
RESULT=$?
echo -n "::group::"
if [ "${RESULT}" -ne 0 ]; then
echo -e "\033[0;31m${testcase} (FAIL)\033[0m"
ERROR="${RESULT}"
else
echo -e "\033[0;32m${testcase} (PASS)\033[0m"
fi
cat /tmp/${testcase}.log
echo "::endgroup::"
done
exit "${ERROR}"

View File

@ -0,0 +1,5 @@
variable "name" {}
output "greeting" {
value = "Hello ${var.name}!"
}

View File

@ -0,0 +1,22 @@
# 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}"
}
}

View File

@ -0,0 +1,10 @@
.
├── module1
│ ├── main.tf
│ ├── main.tftest.hcl
│ ├── foo.tf
│ ├── foo.tftest.hcl
│ ├── bar.tf
│ └── bar.tftest.hcl
└── module2
└── ...

View File

@ -0,0 +1,7 @@
.
├── main.tf
├── main.tftest.hcl
├── foo.tf
├── foo.tftest.hcl
├── bar.tf
└── bar.tftest.hcl

View File

@ -0,0 +1,375 @@
---
description: >-
The tofu test command performs integration tests of OpenTofu modules.
---
import CodeBlock from '@theme/CodeBlock';
import SimpleMain from '!!raw-loader!./examples/simple/main.tf'
import SimpleTest from '!!raw-loader!./examples/simple/main.tftest.hcl'
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import FlatLayout from '!!raw-loader!./flat-layout.txt'
import FlatLayoutModule from '!!raw-loader!./flat-layout-module.txt'
import NestedLayout from '!!raw-loader!./nested-layout.txt'
import NestedLayoutModule from '!!raw-loader!./nested-layout-module.txt'
import ModuleHarness from '!!raw-loader!./examples/module/test-harness/helper.tf'
import ModuleMain from '!!raw-loader!./examples/module/main.tf'
import ModuleTest from '!!raw-loader!./examples/module/main.tftest.hcl'
import PlanMain from '!!raw-loader!./examples/plan/main.tf'
import PlanTest from '!!raw-loader!./examples/plan/main.tftest.hcl'
import OfflineMain from '!!raw-loader!./examples/offline/main.tf'
import OfflineTest from '!!raw-loader!./examples/offline/main.tftest.hcl'
import ProviderAliasMain from '!!raw-loader!./examples/provider_alias/main.tf'
import ProviderAliasTest from '!!raw-loader!./examples/provider_alias/main.tftest.hcl'
import VariablesMain from '!!raw-loader!./examples/variables/main.tf'
import VariablesTest from '!!raw-loader!./examples/variables/main.tftest.hcl'
import ExpectFailureVariablesMain from '!!raw-loader!./examples/expect_failures_variables/main.tf'
import ExpectFailureVariablesTest from '!!raw-loader!./examples/expect_failures_variables/main.tftest.hcl'
import ExpectFailureResourcesMain from '!!raw-loader!./examples/expect_failures_resources/main.tf'
import ExpectFailureResourcesTest from '!!raw-loader!./examples/expect_failures_resources/main.tftest.hcl'
# 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](#options) below.
:::info 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`.
<Tabs>
<TabItem label={"main.tf"} value={"main"} default>
<CodeBlock language="hcl">{SimpleMain}</CodeBlock>
</TabItem>
<TabItem label={"main.tftest.hcl"} value={"test"}>
<CodeBlock language="hcl">{SimpleTest}</CodeBlock>
</TabItem>
</Tabs>
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 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:
<Tabs>
<TabItem value="flat" label="Flat layout" default>
This layout places the <code>*.tftest.hcl</code> test files directly next to the <code>*.tf</code> files they
test. There are no rules that each <code>*.tf</code> file must have its own test file, but it is a good practice
to follow.
<CodeBlock>{FlatLayout}</CodeBlock>
</TabItem>
<TabItem value="nested" label="Nested layout">
This layout places the <code>*.tftest.hcl</code> files in a separate <code>tests</code> directory. Similar to
the flat layout, there are no rules that each <code>*.tf</code> file must have its own test file, but it is a
good practice to follow.
<CodeBlock>{NestedLayout}</CodeBlock>
</TabItem>
</Tabs>
### Testing modules
When testing modules, you can use one of the above directory structures for each module:
<Tabs>
<TabItem value="flat" label="Flat layout" default>
With this layout, run <code>tofu test -test-directory=./path/to/module</code> to test the module in question.
<CodeBlock>{FlatLayoutModule}</CodeBlock>
</TabItem>
<TabItem value="nested" label="Nested layout">
With this layout, <strong>change your working directory to your module path</strong> and
run <code>tofu test</code> to test the module in question.
<CodeBlock>{NestedLayoutModule}</CodeBlock>
</TabItem>
</Tabs>
:::tip 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` 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](#the-run-block)**: define your tests.
* A **[`variables` block](#the-variables-and-runvariables-blocks)** (optional): define variables for all tests in the
current file.
* The **[`provider` blocks](#the-providers-block)** (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`](#the-runassert-block) | 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`](#the-runmodule-block) | block | Overrides the module being tested. You can use this to load a helper module for more elaborate tests. |
| [`expect_failures`](#the-runexpect_failures-list) | list | A list of resources that should fail to provision in the current run. |
| [`variables`](#the-variables-and-runvariables-blocks) | block | Defines variables for the current test case. See the [variables section](#variables). |
| [`command`](#the-runcommand-setting-and-the-runplan_options-block) | `plan` or `apply` | Defines the command which OpenTofu will execute, `plan` or `apply`. Defaults to `apply`. |
| [`plan_options`](#the-runcommand-setting-and-the-runplan_options-block) | block | Options for the `plan` or `apply` operation. |
| [`providers`](#the-providers-block) | 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:
1. The `condition` is an [OpenTofu condition](/docs/language/expressions/custom-conditions#condition-expressions) 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.
:::tip Example
As a simple example, you can write an `assert` block as follows:
<Tabs>
<TabItem label={"main.tftest.hcl"} value={"test"} default>
<CodeBlock language="hcl">{SimpleTest}</CodeBlock>
</TabItem>
<TabItem label={"main.tf"} value={"main"}>
<CodeBlock language="hcl">{SimpleMain}</CodeBlock>
</TabItem>
</Tabs>
:::
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](#the-runmodule-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](/docs/language/expressions/custom-conditions#condition-expressions) 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:
```hcl
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](/docs/language/modules/sources).
* The `version` specifies the version of the module you want to use.
:::warning 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](#the-variables-and-runvariables-blocks) to pass parameters to your module.
:::
:::tip 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.
<Tabs>
<TabItem value={"main"} label={"main.tf"} default>
<CodeBlock language={"hcl"}>{ModuleMain}</CodeBlock>
</TabItem>
<TabItem value={"test"} label={"main.tftest.hcl"}>
<CodeBlock language={"hcl"}>{ModuleTest}</CodeBlock>
</TabItem>
<TabItem value={"helper"} label={"test-harness/helper.tf"} default>
<CodeBlock language={"hcl"}>{ModuleHarness}</CodeBlock>
</TabItem>
</Tabs>
This project uses a [third-party provider](https://github.com/kreuzwerker/terraform-provider-docker) 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:
<Tabs>
<TabItem value="test" label="main.tftest.hcl" default>
<CodeBlock language={"hcl"}>{VariablesTest}</CodeBlock>
</TabItem>
<TabItem value="main" label="main.tf"><CodeBlock language={"hcl"}>{VariablesMain}</CodeBlock></TabItem>
</Tabs>
### 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:
<Tabs>
<TabItem value={"test"} label={"main.tftest.hcl"} default>
<CodeBlock language={"hcl"}>{ExpectFailureVariablesTest}</CodeBlock>
</TabItem>
<TabItem value={"main"} label={"main.tf"}>
<CodeBlock language={"hcl"}>{ExpectFailureVariablesMain}</CodeBlock>
</TabItem>
</Tabs>
You can also use the `expect_failure` clause to check [lifecycle](/docs/language/meta-arguments/lifecycle/) events like
pre- or postconditions as well as the results of checks.
:::warning Limitation
The `expect_failure` list currently does not support testing resource creation failures. You must provide a
[lifecycle](/docs/language/meta-arguments/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.
<Tabs>
<TabItem value={"test"} label={"main.tftest.hcl"} default>
<CodeBlock language={"hcl"}>{ExpectFailureResourcesTest}</CodeBlock>
</TabItem>
<TabItem value={"main"} label={"main.tf"}>
<CodeBlock language={"hcl"}>{ExpectFailureResourcesMain}</CodeBlock>
</TabItem>
</Tabs>
### 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:
<Tabs>
<TabItem value={"test"} label={"main.tftest.hcl"} default>
<CodeBlock language={"hcl"}>{PlanTest}</CodeBlock>
</TabItem>
<TabItem value={"main"} label={"main.tf"}>
<CodeBlock language={"hcl"}>{PlanMain}</CodeBlock>
</TabItem>
</Tabs>
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 Tip
You can use these options in conjunction with [provider overrides](#the-providers-block) to create fully offline tests. See the
[Providers section below](#the-providers-block) 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.
```hcl
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:
<Tabs>
<TabItem value={"test"} label={"main.tftest.hcl"} default>
<CodeBlock language={"hcl"}>{OfflineTest}</CodeBlock>
</TabItem>
<TabItem value={"main"} label={"main.tf"}>
<CodeBlock language={"hcl"}>{OfflineMain}</CodeBlock>
</TabItem>
</Tabs>
#### 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.
<Tabs>
<TabItem value={"test"} label={"main.tftest.hcl"} default>
<CodeBlock language={"hcl"}>{ProviderAliasTest}</CodeBlock>
</TabItem>
<TabItem value={"main"} label={"main.tf"}>
<CodeBlock language={"hcl"}>{ProviderAliasMain}</CodeBlock>
</TabItem>
</Tabs>

View File

@ -0,0 +1,11 @@
.
├── module1
│ ├── main.tf
│ ├── foo.tf
│ ├── bar.tf
│ └── tests
│ ├── main.tftest.hcl
│ ├── foo.tftest.hcl
│ └── bar.tftest.hcl
└── module2
└── ...

View File

@ -0,0 +1,8 @@
.
├── main.tf
├── foo.tf
├── bar.tf
└── tests
├── main.tftest.hcl
├── foo.tftest.hcl
└── bar.tftest.hcl