命令:test
tofu test
命令允许您通过创建真实的 基础设施并检查是否满足所需的条件(断言)来测试您的 OpenTofu 配置。测试完成后,OpenTofu 会销毁它创建的资源。
用法
用法:tofu test [选项]
。
此命令将执行当前目录或名为 tests
的目录中的所有 *.tftest.hcl
、*.tftest.json
、*.tofutest.hcl
和 *.tofutest.json
文件。您可以使用下面的 选项 自定义此行为。
考虑以下简单示例,它从 main.tf
创建一个 test.txt
文件,然后从 main.tftest.hcl
检查主代码是否已成功执行其工作。
- main.tf
- main.tftest.hcl
resource "local_file" "test" {
filename = "${path.module}/test.txt"
content = "Hello world!"
}
run "test" {
assert {
condition = file(local_file.test.filename) == "Hello world!"
error_message = "Incorrect content in ${local_file.test.filename}."
}
}
您可以运行 tofu init
,然后运行 tofu test
来执行测试,该测试将应用 main.tf
文件并根据 main.tftest.hcl
中的断言对其进行测试。这只是一个简单的示例。您可以在下面找到更全面的示例。
扩展优先级
当同一目录中存在具有相同基本名称的 .tftest.hcl
和 .tofutest.hcl
文件时,OpenTofu 将优先考虑 .tofutest.hcl
文件并忽略 .tftest.hcl
文件。例如
- 如果
main.tftest.hcl
和main.tofutest.hcl
都存在于同一目录中,则 OpenTofu 将仅加载main.tofutest.hcl
并忽略main.tftest.hcl
。
这确保了当两种文件都可用时,.tofu
文件始终优先于 .tf
文件。对于希望其模块同时支持 OpenTofu 和 Terraform 并希望为每个模块创建不同测试的模块作者来说,这种情况非常有用。
相同的规则适用于基于 JSON 的测试文件
- 如果
main.tftest.json
和main.tofutest.json
都存在于同一目录中,则 OpenTofu 将仅加载main.tofutest.json
并忽略main.tftest.json
。
选项
-test-directory=path
设置测试目录(默认值:“tests”)。当您运行tofu test
时,OpenTofu 将在指定的目录以及当前目录中搜索测试文件。路径应相对于当前工作目录。-filter=testfile
指定要运行的单个测试文件。多次使用此选项以指定多个文件。路径应相对于当前工作目录。-var 'foo=bar'
设置根模块的输入变量。多次指定此选项以添加多个变量。-var-file=filename
从指定的文件设置多个变量。除了此文件之外,OpenTofu 还自动加载terraform.tfvars
和*.auto.tfvars
。多次使用此选项以指定多个文件。-json
将输出格式更改为 JSON。-no-color
在命令输出中禁用彩色输出。-verbose
打印每个测试运行块在执行时的计划或状态。
目录结构
tofu test
命令支持两种目录布局:扁平化或嵌套。
- 扁平化布局
- 嵌套布局
这种布局将 *.tftest.hcl
测试文件直接放在与其测试的 *.tf
文件旁边。没有规则要求每个 *.tf
文件都必须有自己的测试文件,但遵循此做法是一个好习惯。
.
├── main.tf
├── main.tftest.hcl
├── foo.tf
├── foo.tftest.hcl
├── bar.tf
└── bar.tftest.hcl
这种布局将 *.tftest.hcl
文件放在单独的 tests
目录中。与扁平化布局类似,没有规则要求每个 *.tf
文件都必须有自己的测试文件,但遵循此做法是一个好习惯。
.
├── main.tf
├── foo.tf
├── bar.tf
└── tests
├── main.tftest.hcl
├── foo.tftest.hcl
└── bar.tftest.hcl
测试模块
测试模块时,您可以为每个模块使用上述目录结构之一。
- 扁平化布局
- 嵌套布局
使用此布局,运行 tofu test -test-directory=./path/to/module
来测试目标模块。
.
├── module1
│ ├── main.tf
│ ├── main.tftest.hcl
│ ├── foo.tf
│ ├── foo.tftest.hcl
│ ├── bar.tf
│ └── bar.tftest.hcl
└── module2
└── ...
使用此布局,**将您的工作目录更改为模块路径**,然后运行 tofu test
来测试目标模块。
.
├── module1
│ ├── main.tf
│ ├── foo.tf
│ ├── bar.tf
│ └── tests
│ ├── main.tftest.hcl
│ ├── foo.tftest.hcl
│ └── bar.tftest.hcl
└── module2
└── ...
您可以使用 -filter=sometest.tftest.hcl
选项来运行有限的测试文件集。多次使用此选项可以运行多个测试文件。
*.tftest.hcl
/ *.tofutest.hcl
文件结构
OpenTofu 的测试语言类似于 OpenTofu 的主要语言,并使用相同的块结构。
一个测试文件由以下部分组成:
- **
run
块**: 定义您的测试。 - **
variables
块** (可选):定义当前文件中所有测试的变量。 - **
provider
块** (可选):定义要用于测试的提供程序。 - **
mock_provider
块** (可选):定义要模拟的提供程序。 - **
override_resource
块** (可选):定义要覆盖的资源。 - **
override_data
块** (可选):定义要覆盖的数据源。 - **
override_module
块** (可选):定义要覆盖的模块调用。
run
块
一个 run
块包含一个单个测试用例,该用例运行 tofu apply
或 tofu plan
,然后评估所有 assert
块。测试完成后,它使用 tofu destroy
删除临时创建的资源。
一个 run
块由以下元素组成:
名称 | 类型 | 描述 |
---|---|---|
assert | 块 | 定义断言,以检查您的代码(例如 main.tf )是否正确创建了基础设施。如果您未指定任何 assert 块,则 OpenTofu 将简单地应用配置,而无需任何断言。 |
module | 块 | 覆盖正在测试的模块。您可以使用它来加载辅助模块以进行更详细的测试。 |
expect_failures | 列表 | 在当前运行中应无法预置的资源列表。 |
variables | 块 | 定义当前测试用例的变量。请参阅变量部分。 |
command | plan 或 apply | 定义 OpenTofu 将执行的命令,plan 或 apply 。默认为 apply 。 |
plan_options | 块 | plan 或 apply 操作的选项。 |
providers | 对象 | 提供程序的别名。 |
override_resource | 块 | 定义要为运行覆盖的资源。 |
override_data | 块 | 定义要为运行覆盖的数据源。 |
override_module | 块 | 定义要为运行覆盖的模块调用。 |
run.assert
块
您可以在 run
块内指定 assert
块来测试 apply
或 plan
操作完成后基础设施的状态。您可以定义的块数量没有理论上的限制。
每个块都需要以下两个属性:
condition
是一个OpenTofu 条件,它应该返回true
以使测试通过,返回false
以使测试失败。该条件**必须**引用主代码中的资源、数据源、变量、输出或模块,否则 OpenTofu 将拒绝运行测试。error_message
是一个字符串,解释测试失败时发生的情况。
作为一个简单的示例,您可以编写如下 assert
块:
- main.tftest.hcl
- main.tf
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!"
}
请注意,条件仅允许您对当前 OpenTofu 状态执行基本检查并使用 OpenTofu 函数。**您不能在测试代码中直接定义其他数据源。**要解决此限制,您可以使用module
块来加载辅助模块。
run.module
块
在某些情况下,您可能会发现条件表达式中提供的工具不足以测试您的代码是否正确创建了基础设施。
您可以使用 module
块来覆盖 tofu test
加载的主模块。这使您有机会创建其他资源或数据源,您可以在 assert
条件中使用这些资源或数据源。
其语法类似于在普通 OpenTofu 代码中加载模块:
run "test" {
module {
source = "./some-module"
}
}
module
块具有以下两个属性:
source
属性指向要加载的模块的目录或任何其他模块源。version
指定您要使用的模块的版本。
您不能像在普通 OpenTofu 代码中那样直接在 module
块中传递参数。相反,您应该使用variables
块将参数传递给您的模块。
在此示例项目中,main.tf
文件创建了一个 Docker 容器,其中包含在端口 8080 上公开的 nginx
镜像。main.tftest.hcl
文件需要测试 Web 服务器是否确实已正确启动,但如果没有辅助模块,它就无法做到这一点。
要创建 http
数据源,main.tftest.hcl
文件加载 test-harness
模块。然后,测试辅助程序加载主模块并将数据源添加到检查 HTTP 响应中。请注意,test-harness
中的数据源对 module.main
具有显式依赖关系,以确保数据源仅在主模块完成其工作后才返回。
- 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
}
}
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 = "https://#:8080"
# Important! Wait for the main module to finish:
depends_on = [module.main]
}
此项目使用第三方提供程序来启动容器。如果您安装了 Docker Engine,则可以在本地运行它。
variables
和 run.variables
块
正在测试的代码(例如 main.tf
)通常具有变量块,您需要从测试用例中填充这些变量。您可以使用以下任何方法为测试运行提供变量:
顺序 | 来源 |
---|---|
1 | 带有 TF_VAR_ 前缀的环境变量。 |
2 | 在当前目录中指定的 tfvar 文件:terraform.tfvars 和 *.auto.tfvars 。 |
3 | 在测试目录中指定的 tfvar 文件:tests/terraform.tfvars 和 tests/*.auto.tfvars 。 |
4 | 使用标志 -var 定义的命令行变量,以及标志 -var-file 指定的文件中定义的变量。 |
5 | 测试文件中的 variables 块中的变量。 |
6 | run 块中的 variables 块中的变量。 |
OpenTofu 按上述列出的顺序评估变量,因此您可以使用它来覆盖先前设置的变量。例如:
- main.tftest.hcl
- main.tf
# 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}!"
}
run.expect_failures
列表
在某些情况下,您可能希望测试代码的故意失败,例如确保您的验证正在工作。
您可以在 run
块内使用 expect_failures
来指定在使用给定参数运行代码时哪些变量或资源应该失败。
例如,下面的测试用例检查当 instances
变量提供负数时是否会正确失败验证:
- main.tftest.hcl
- main.tf
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"
}
}
您还可以使用 expect_failure
子句来检查生命周期事件(如前提条件或后置条件)以及检查结果。
expect_failure
列表目前不支持测试资源创建失败。您必须提供生命周期事件才能使用 expect_failure
。
下面的示例检查配置错误的健康检查是否失败。这确保了即使健康检查针对错误的端点运行,它也不会始终返回。
- main.tftest.hcl
- main.tf
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 = "https://#: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}"
}
}
run.command
设置和 run.plan_options
块
默认情况下,tofu test
使用 tofu apply
创建真实的基础设施。在某些情况下,例如如果真实基础设施非常昂贵或无法用于测试目的,则仅运行 tofu plan
可能很有用。您可以使用 command = plan
设置来执行计划而不是应用。以下示例测试变量是否正确传递到 docker_image
资源,而无需实际应用计划:
- main.tftest.hcl
- main.tf
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 = "."
}
}
无论 command
设置如何,您都可以使用 plan_options
块为这两种模式指定以下其他选项:
名称 | 描述 |
---|---|
mode | 将此选项从 normal (默认值)更改为 refresh-only 以仅从远程基础设施刷新本地状态。 |
refresh | 将此选项设置为 false 以禁用检查与状态文件相关的外部更改。类似于 tofu plan -refresh=false 。 |
replace | 强制替换指定的资源列表,例如上述示例中的 [docker_image.build] 。类似于 tofu plan -replace=docker_image.build 。 |
target | 将计划限制为指定的模块或资源列表。类似于 tofu plan -target=docker_image.build 。 |
providers
块
在某些情况下,您可能希望覆盖测试运行的提供程序设置。您可以使用run
块之外的provider
块为提供程序提供其他配置选项,例如测试帐户的凭据。
provider "aws" {
// Add additional settings here
}
如果提供程序支持,此功能还可以启用部分或完全离线测试。以下示例说明了使用 AWS 提供程序和 S3 存储桶资源的完全离线测试
- main.tftest.hcl
- main.tf
// 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
}
提供程序别名
除了提供程序覆盖之外,您还可以为提供程序设置别名,以便在您的run
块中用不同的提供程序替换它们。当您希望在同一个测试文件中拥有两个提供程序配置并在它们之间切换时,这很有用。
在下面的示例中,sockettest
测试用例加载了与文件其余部分不同的 Docker 提供程序配置。
- main.tftest.hcl
- main.tf
# 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 = "."
}
}
mock_provider
块
mock_provider
块允许您用模拟的块替换提供程序配置。在这种情况下,将跳过提供程序资源和数据源的创建和检索。相反,OpenTofu 将自动生成所有计算属性和块以用于测试。
详细了解 OpenTofu 如何生成自动生成的数值。
模拟提供程序也支持alias
字段以及mock_resource
和mock_data
块。在某些情况下,您可能希望使用默认值而不是自动生成的数值,方法是将它们传递到mock_resource
或mock_data
块的defaults
字段中。
在下面的示例中,我们测试存储桶名称是否正确传递给资源,而无需实际创建它
- main.tftest.hcl
- main.tf
// All resources and data sources provided by `aws.mock` provider
// will be mocked. Their values will be automatically generated.
mock_provider "aws" {
alias = "mock"
}
// The same goes for `local` provider. Also, every `local_file`
// data source will have its `content` set to `test`.
mock_provider "local" {
mock_data "local_file" {
defaults = {
content = "test"
}
}
}
// Test if the bucket name is correctly passed to the aws_s3_bucket
// resource from the local file.
run "test" {
// Use `aws.mock` provider for this test run only.
providers = {
aws = aws.mock
}
assert {
condition = aws_s3_bucket.test.bucket == "test"
error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
}
}
data "local_file" "bucket_name" {
filename = "bucket_name.txt"
}
provider "aws" {
region = "us-east-2"
}
resource "aws_s3_bucket" "test" {
bucket = data.local_file.bucket_name.content
}
override_resource
和 override_data
块
在某些情况下,您可能希望使用某些被覆盖的资源或数据源来测试您的基础设施。您可以使用override_resource
或override_data
块来跳过使用真实提供程序创建和检索这些资源或数据源。相反,OpenTofu 将自动生成所有计算属性和块以用于测试。
详细了解 OpenTofu 如何生成自动生成的数值。
这些块包含以下元素
名称 | 类型 | 描述 |
---|---|---|
target | 引用 | 必需。要覆盖的目标资源或数据源的地址。 |
数值 | 对象 | 要用于自动生成的计算属性和块的自定义值。 |
您可以对整个测试文件或单个run
块使用override_resource
或override_data
块。如果两者都为同一个target
指定,则后者优先。
在下面的示例中,我们测试存储桶名称是否正确传递给资源,而无需实际创建它
- main.tftest.hcl
- main.tf
// This data source will not be called for any run
// in this `.tftest.hcl` file. Instead, `values` object
// will be used to populate `content` attribute. Other
// attributes and blocks will be automatically generated.
override_data {
target = data.local_file.bucket_name
values = {
content = "test"
}
}
// Test if the bucket name is correctly passed to the aws_s3_bucket
// resource from the local file.
run "test" {
// S3 bucket will not be created in AWS for this run,
// but it's available to use in both tests and configuration.
override_resource {
target = aws_s3_bucket.test
}
assert {
condition = aws_s3_bucket.test.bucket == "test"
error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
}
}
data "local_file" "bucket_name" {
filename = "bucket_name.txt"
}
provider "aws" {
region = "us-east-2"
}
resource "aws_s3_bucket" "test" {
bucket = data.local_file.bucket_name.content
}
您不能将override_resource
或override_data
与资源或数据源的单个实例一起使用。必须覆盖资源或数据源的每个实例。
自动生成的数值
模拟资源和数据源需要 OpenTofu 在不调用相应提供程序的情况下自动生成计算属性。在生成这些值时,OpenTofu 无法遵循自定义提供程序逻辑,因此它使用基于值类型的简单规则
属性类型 | 生成的值 |
---|---|
数字 | 0 |
布尔值 | 假 |
字符串 | 一个随机的字母数字字符串。 |
列表 | 一个空列表。 |
映射 | 一个空映射。 |
集合 | 一个空集合。 |
对象 | 一个对象,其字段通过相同的逻辑递归填充。 |
元组 | 一个空元组。 |
您可以设置自定义值以代替通过相应的模拟或覆盖字段自动生成的值。请记住,这仅适用于计算属性,配置值无法更改。
override_module
块
在某些情况下,您可能希望使用某些被覆盖的模块调用来测试您的基础设施。您可以使用override_module
块忽略被调用模块提供的全部配置。在这种情况下,OpenTofu 将使用override_module
块中指定的自定义值作为模块输出。
该块包含以下元素
名称 | 类型 | 描述 |
---|---|---|
target | 引用 | 必需。要覆盖的目标模块调用的地址。 |
输出 | 对象 | 用作模块调用输出的值。如果未指定输出,则 OpenTofu 默认将其设置为null 。 |
您可以对整个测试文件或单个run
块使用override_module
块。如果两者都为同一个target
指定,则后者优先。
在下面的示例中,我们测试存储桶名称是否正确地从模块传递,而无需实际调用它
- main.tftest.hcl
- main.tf
- bucket_meta/main.tf
// All the module configuration will be ignored for this
// module call. Instead, the `outputs` object will be used
// to populate module outputs.
override_module {
target = module.bucket_meta
outputs = {
name = "test"
tags = {
Environment = "Test Env"
}
}
}
// Test if the bucket name is correctly passed to the aws_s3_bucket
// resource from the module call.
run "test" {
// S3 bucket will not be created in AWS for this run,
// but it's available to use in both tests and configuration.
override_resource {
target = aws_s3_bucket.test
}
assert {
condition = aws_s3_bucket.test.bucket == "test"
error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
}
assert {
condition = aws_s3_bucket.test.tags["Environment"] == "Test Env"
error_message = "Incorrect `Environment` tag: ${aws_s3_bucket.test.tags["Environment"]}"
}
}
module "bucket_meta" {
source = "./bucket_meta"
}
provider "aws" {
region = "us-east-2"
}
resource "aws_s3_bucket" "test" {
bucket = module.bucket_meta.name
tags = module.bucket_meta.tags
}
data "local_file" "bucket_name" {
filename = "bucket_name.txt"
}
output "name" {
value = data.local_file.bucket_name.content
}
output "tags" {
value = {
Environment = "Dev"
}
}
您不能将override_module
与模块调用的单个实例一起使用。必须覆盖模块调用的每个实例。