关于 terraform-test
terraform-test 是一项指导使用 Terraform 内置测试框架检查配置行为和变更的技能,面向 Terraform 配置维护者,涵盖 plan、apply、断言、模拟 Provider 及 CI/CD 测试。
主要功能
- 01Plan 和 apply 测试场景
- 02用于配置检查的断言
- 03模拟 Provider 指导
- 04变量与失败测试模式
- 05测试筛选和清理命令
- 06CI/CD 参考指导
适用场景
- 01检查模块输出和条件
- 02用无效输入测试验证规则
- 03将快速单元测试与集成测试分开
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ hashicorp/ agent-skills --skill terraform-test安装后在 Claude Code 中输入/terraform-test确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/CI_CD.mdEXAMPLES.mdMOCK_PROVIDERS.md详细介绍
Terraform 测试
Terraform 内置的测试框架可验证配置更新不会引入破坏性变更。测试会针对临时资源运行,保护现有基础设施和状态文件。
参考文件
references/MOCK_PROVIDERS.md— Mock provider 语法、常见默认值、何时使用 mock(仅适用于 Terraform 1.7.0 及更高版本——如果用户的版本低于 1.7,请跳过)references/CI_CD.md— GitHub Actions 和 GitLab CI 流水线示例references/EXAMPLES.md— 完整的测试套件示例(针对 VPC 模块的单元测试、集成测试和 mock 测试)
用户询问 mock、CI/CD 集成或需要完整示例时,请阅读相关参考文件。
核心概念
- 测试文件(
.tftest.hcl/.tftest.json):包含用于验证配置的run块 - Run 块:单个测试场景,可选包含变量、provider 和断言
- Assert 块:测试通过所必须满足的条件
- Mock provider:模拟 provider 行为,无需真实基础设施(Terraform 1.7.0 及更高版本)
- 测试模式:
apply(默认,会创建真实资源)或plan(仅验证逻辑)
文件结构
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── defaults_unit_test.tftest.hcl # plan mode — fast, no resources
├── validation_unit_test.tftest.hcl # plan mode
└── full_stack_integration_test.tftest.hcl # apply mode — creates real resources
对于 plan 模式测试,请使用 *_unit_test.tftest.hcl;对于 apply 模式测试,请使用 *_integration_test.tftest.hcl,以便在 CI 中分别筛选。
测试文件结构
# Optional: test-wide settings
test {
parallel = true # Enable parallel execution for all run blocks (default: false)
}
# Optional: file-level variables (highest precedence, override all other sources)
variables {
aws_region = "us-west-2"
instance_type = "t2.micro"
}
# Optional: provider configuration
provider "aws" {
region = var.aws_region
}
# Required: at least one run block
run "test_default_configuration" {
command = plan
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Instance type should be t2.micro by default"
}
}
Run 块
run "test_name" {
command = plan # or apply (default)
parallel = true # optional, since v1.9.0
# Override file-level variables
variables {
instance_type = "t3.large"
}
# Reference a specific module
module {
source = "./modules/vpc" # local or registry only (not git/http)
version = "5.0.0" # registry modules only
}
# Control state isolation
state_key = "shared_state" # since v1.9.0
# Plan behavior
plan_options {
mode = refresh-only # or normal (default)
refresh = true
replace = [aws_instance.example]
target = [aws_instance.example]
}
# Assertions
assert {
condition = aws_instance.example.id != ""
error_message = "Instance should have a valid ID"
}
# Expected failures (test passes if these fail)
expect_failures = [
var.instance_count
]
}
常见测试模式
验证输出
run "test_outputs" {
command = plan
assert {
condition = output.vpc_id != null
error_message = "VPC ID output must be defined"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID should start with 'vpc-'"
}
}
条件资源
run "test_nat_gateway_disabled" {
command = plan
variables {
create_nat_gateway = false
}
assert {
condition = length(aws_nat_gateway.main) == 0
error_message = "NAT gateway should not be created when disabled"
}
}
资源数量
run "test_resource_count" {
command = plan
variables {
instance_count = 3
}
assert {
condition = length(aws_instance.workers) == 3
error_message = "Should create exactly 3 worker instances"
}
}
标签
run "test_resource_tags" {
command = plan
variables {
common_tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Environment tag should be set correctly"
}
assert {
condition = aws_instance.example.tags["ManagedBy"] == "Terraform"
error_message = "ManagedBy tag should be set correctly"
}
}
数据源
run "test_data_source_lookup" {
command = plan
assert {
condition = data.aws_ami.ubuntu.id != ""
error_message = "Should find a valid Ubuntu AMI"
}
assert {
condition = can(regex("^ami-", data.aws_ami.ubuntu.id))
error_message = "AMI ID should be in correct format"
}
}
验证规则
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
带依赖关系的顺序测试
run "setup_vpc" {
command = apply
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_subnet_in_vpc" {
command = plan
variables {
vpc_id = run.setup_vpc.vpc_id
}
assert {
condition = aws_subnet.example.vpc_id == run.setup_vpc.vpc_id
error_message = "Subnet should be in the VPC from setup_vpc"
}
}
Plan 选项(仅刷新、指定目标)
run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Tags should be refreshed correctly"
}
}
run "test_specific_resource" {
command = plan
plan_options {
target = [aws_instance.example]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Targeted resource should be planned"
}
}
并行模块
run "test_networking_module" {
command = plan
parallel = true
module {
source = "./modules/networking"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_compute_module" {
command = plan
parallel = true
module {
source = "./modules/compute"
}
assert {
condition = output.instance_id != ""
error_message = "Instance should be created"
}
}
共享状态键
run "create_foundation" {
command = apply
state_key = "foundation"
assert {
condition = aws_vpc.main.id != ""
error_message = "Foundation VPC should be created"
}
}
run "create_application" {
command = apply
state_key = "foundation"
variables {
vpc_id = run.create_foundation.vpc_id
}
assert {
condition = aws_instance.app.vpc_id == run.create_foundation.vpc_id
error_message = "Application should use foundation VPC"
}
}
清理顺序(先清理 S3 对象,再清理存储桶)
run "create_bucket" {
command = apply
assert {
condition = aws_s3_bucket.example.id != ""
error_message = "Bucket should be created"
}
}
run "add_objects" {
command = apply
assert {
condition = length(aws_s3_object.files) > 0
error_message = "Objects should be added"
}
}
# Cleanup destroys in reverse: objects first, then bucket
多个别名 provider
provider "aws" {
alias = "primary"
region = "us-west-2"
}
provider "aws" {
alias = "secondary"
region = "us-east-1"
}
run "test_with_specific_provider" {
command = plan
providers = {
aws = provider.aws.secondary
}
assert {
condition = aws_instance.example.availability_zone == "us-east-1a"
error_message = "Instance should be in us-east-1 region"
}
}
复杂条件
assert {
condition = alltrue([
for subnet in aws_subnet.private :
can(regex("^10\\.0\\.", subnet.cidr_block))
])
error_message = "All private subnets should use 10.0.0.0/8 CIDR range"
}
清理
测试完成后,资源会按 run 块的逆序销毁。这对依赖关系很重要(例如,先清理 S3 对象,再清理存储桶)。使用 terraform test -no-cleanup 可跳过清理以便调试。
运行测试
terraform test # all tests
terraform test tests/defaults.tftest.hcl # specific file
terraform test -filter=test_vpc_configuration # by run block name
terraform test -test-directory=integration-tests # custom directory
terraform test -verbose # detailed output
terraform test -no-cleanup # skip resource cleanup
最佳实践
- 命名:plan 模式使用
*_unit_test.tftest.hcl,apply 模式使用*_integration_test.tftest.hcl - 测试命名:使用能说明测试场景的描述性 run 块名称
- 默认使用 plan:除非需要测试真实资源行为,否则使用
command = plan - 使用 mock处理外部依赖——速度更快且无需凭据(参见
references/MOCK_PROVIDERS.md) - 错误消息:提供足够具体的信息,以便诊断失败,而无需重新运行测试
- 负向测试:使用
expect_failures验证验证规则会拒绝错误输入 - 变量覆盖:测试不同的变量组合,以验证所有代码路径——测试变量的优先级最高,会覆盖所有其他来源
- 模块源:测试文件仅支持本地路径和 registry 模块,不支持 git 或 HTTP URL
- 并行执行:对于使用不同状态文件的独立测试,请使用
parallel = true - 清理:集成测试会自动按 run 块的逆序销毁资源;使用
-no-cleanup进行调试 - CI/CD:每个 PR 都运行单元测试,合并时运行集成测试(参见
references/CI_CD.md)
故障排查
| 问题 | 解决方案 |
|---|---|
| 断言失败 | 使用 -verbose 查看实际值与预期值 |
| 缺少凭据 | 单元测试使用 mock provider |
| 不支持的模块源 | 将 git/HTTP 源转换为本地模块 |
| 测试相互干扰 | 使用 state_key 或单独的模块进行隔离 |
| 测试速度慢 | 使用 command = plan 和 mock;单独运行集成测试 |
参考资料
评论
支持 Markdown