mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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:
parent
aabe1c95be
commit
e2613d7bf0
@ -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"`.
|
||||
:::
|
4
website/docs/cli/commands/test/examples/.gitignore
vendored
Normal file
4
website/docs/cli/commands/test/examples/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.terraform
|
||||
.terraform.lock.hcl
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
@ -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}"
|
||||
}
|
||||
}
|
@ -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
|
||||
]
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
variable "instances" {
|
||||
type = number
|
||||
|
||||
validation {
|
||||
condition = var.instances >= 0
|
||||
error_message = "The number of instances must be positive or zero"
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
run "main" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
instances = -1
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.instances,
|
||||
]
|
||||
}
|
17
website/docs/cli/commands/test/examples/module/main.tf
Normal file
17
website/docs/cli/commands/test/examples/module/main.tf
Normal 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
|
||||
}
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
module "greet" {
|
||||
source = "./module"
|
||||
name = "greet"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
run "test" {
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
variable "name" {}
|
||||
|
||||
output "greeting" {
|
||||
value = "Hello ${var.name}!"
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
run "test" {
|
||||
module {
|
||||
source="../"
|
||||
}
|
||||
variables {
|
||||
name = "OpenTofu"
|
||||
}
|
||||
assert {
|
||||
condition = output.greeting == "Hello OpenTofu!"
|
||||
error_message = "Incorrect greeting: ${output.greeting}"
|
||||
}
|
||||
}
|
9
website/docs/cli/commands/test/examples/offline/main.tf
Normal file
9
website/docs/cli/commands/test/examples/offline/main.tf
Normal file
@ -0,0 +1,9 @@
|
||||
variable "bucket_name" {}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-2"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = var.bucket_name
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
3
website/docs/cli/commands/test/examples/plan/Dockerfile
Normal file
3
website/docs/cli/commands/test/examples/plan/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM nginx
|
||||
|
||||
# It doesn't really matter what we do here, it won't run anyway.
|
19
website/docs/cli/commands/test/examples/plan/main.tf
Normal file
19
website/docs/cli/commands/test/examples/plan/main.tf
Normal 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 = "."
|
||||
}
|
||||
}
|
13
website/docs/cli/commands/test/examples/plan/main.tftest.hcl
Normal file
13
website/docs/cli/commands/test/examples/plan/main.tftest.hcl
Normal 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"
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
FROM nginx
|
@ -0,0 +1,15 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
version = "3.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_image" "build" {
|
||||
name = "myapp"
|
||||
build {
|
||||
context = "."
|
||||
}
|
||||
}
|
@ -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.
|
4
website/docs/cli/commands/test/examples/simple/main.tf
Normal file
4
website/docs/cli/commands/test/examples/simple/main.tf
Normal file
@ -0,0 +1,4 @@
|
||||
resource "local_file" "test" {
|
||||
filename = "${path.module}/test.txt"
|
||||
content = "Hello world!"
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
run "test" {
|
||||
assert {
|
||||
condition = file(local_file.test.filename) == "Hello world!"
|
||||
error_message = "Incorrect content in ${local_file.test.filename}."
|
||||
}
|
||||
}
|
44
website/docs/cli/commands/test/examples/test.sh
Executable file
44
website/docs/cli/commands/test/examples/test.sh
Executable 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}"
|
@ -0,0 +1,5 @@
|
||||
variable "name" {}
|
||||
|
||||
output "greeting" {
|
||||
value = "Hello ${var.name}!"
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
10
website/docs/cli/commands/test/flat-layout-module.txt
Normal file
10
website/docs/cli/commands/test/flat-layout-module.txt
Normal file
@ -0,0 +1,10 @@
|
||||
.
|
||||
├── module1
|
||||
│ ├── main.tf
|
||||
│ ├── main.tftest.hcl
|
||||
│ ├── foo.tf
|
||||
│ ├── foo.tftest.hcl
|
||||
│ ├── bar.tf
|
||||
│ └── bar.tftest.hcl
|
||||
└── module2
|
||||
└── ...
|
7
website/docs/cli/commands/test/flat-layout.txt
Normal file
7
website/docs/cli/commands/test/flat-layout.txt
Normal file
@ -0,0 +1,7 @@
|
||||
.
|
||||
├── main.tf
|
||||
├── main.tftest.hcl
|
||||
├── foo.tf
|
||||
├── foo.tftest.hcl
|
||||
├── bar.tf
|
||||
└── bar.tftest.hcl
|
375
website/docs/cli/commands/test/index.mdx
Normal file
375
website/docs/cli/commands/test/index.mdx
Normal 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>
|
11
website/docs/cli/commands/test/nested-layout-module.txt
Normal file
11
website/docs/cli/commands/test/nested-layout-module.txt
Normal file
@ -0,0 +1,11 @@
|
||||
.
|
||||
├── module1
|
||||
│ ├── main.tf
|
||||
│ ├── foo.tf
|
||||
│ ├── bar.tf
|
||||
│ └── tests
|
||||
│ ├── main.tftest.hcl
|
||||
│ ├── foo.tftest.hcl
|
||||
│ └── bar.tftest.hcl
|
||||
└── module2
|
||||
└── ...
|
8
website/docs/cli/commands/test/nested-layout.txt
Normal file
8
website/docs/cli/commands/test/nested-layout.txt
Normal file
@ -0,0 +1,8 @@
|
||||
.
|
||||
├── main.tf
|
||||
├── foo.tf
|
||||
├── bar.tf
|
||||
└── tests
|
||||
├── main.tftest.hcl
|
||||
├── foo.tftest.hcl
|
||||
└── bar.tftest.hcl
|
Loading…
Reference in New Issue
Block a user