terraform-test by hashicorp/agent-skills
npx skills add https://github.com/hashicorp/agent-skills --skill terraform-testTerraform 内置的测试框架使模块作者能够验证配置更新不会引入破坏性变更。测试针对临时资源执行,保护现有基础设施和状态文件。
测试文件:包含测试配置和运行块的 .tftest.hcl 或 .tftest.json 文件,用于验证您的 Terraform 配置。
测试块:定义测试范围设置的可选配置块(自 Terraform 1.6.0 起可用)。
运行块:定义单个测试场景,包含可选的变量、提供程序配置和断言。每个测试文件至少需要一个运行块。
断言块:包含必须评估为真才能使测试通过的条件。失败的断言会导致测试失败。
模拟提供程序:模拟提供程序行为而无需创建真实基础设施(自 Terraform 1.7.0 起可用)。
测试模式:测试在应用模式(默认,创建真实基础设施)或计划模式(验证逻辑而不创建资源)下运行。
Terraform 测试文件使用 .tftest.hcl 或 .tftest.json 扩展名,通常组织在 tests/ 目录中。使用清晰的命名约定来区分单元测试(计划模式)和集成测试(应用模式):
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── validation_unit_test.tftest.hcl # 单元测试(计划模式)
├── edge_cases_unit_test.tftest.hcl # 单元测试(计划模式)
└── full_stack_integration_test.tftest.hcl # 集成测试(应用模式 - 创建真实资源)
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
测试文件包含:
test 块(配置设置)run 块(测试执行)variables 块(输入值)provider 块(提供程序配置)mock_provider 块(模拟提供程序数据,自 v1.7.0 起)重要:variables 和 provider 块的顺序无关紧要。Terraform 在测试操作开始时处理这些块内的所有值。
可选的 test 块配置测试范围的设置:
test {
parallel = true # 为所有运行块启用并行执行(默认值:false)
}
测试块属性:
parallel - 布尔值,设置为 true 时,默认启用所有运行块的并行执行(默认值:false)。单个运行块可以覆盖此设置。每个 run 块对您的配置执行一个命令。运行块默认按顺序执行。
基本集成测试(应用模式 - 默认):
run "test_instance_creation" {
command = apply
assert {
condition = aws_instance.example.id != ""
error_message = "实例应创建有效的 ID"
}
assert {
condition = output.instance_public_ip != ""
error_message = "实例应具有公共 IP"
}
}
单元测试(计划模式):
run "test_default_configuration" {
command = plan
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "实例类型默认应为 t2.micro"
}
assert {
condition = aws_instance.example.tags["Environment"] == "test"
error_message = "环境标签应为 'test'"
}
}
运行块属性:
command - 可以是 apply(默认)或 planplan_options - 配置计划行为(见下文)variables - 覆盖测试级别的变量值module - 引用用于测试的备用模块providers - 自定义提供程序可用性assert - 验证条件(允许多个)expect_failures - 指定预期的验证失败state_key - 管理状态文件隔离(自 v1.9.0 起)parallel - 设置为 true 时启用并行执行(自 v1.9.0 起)plan_options 块配置计划命令行为:
run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only # "normal"(默认)或 "refresh-only"
refresh = true # 布尔值,默认为 true
replace = [
aws_instance.example
]
target = [
aws_instance.example
]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "实例类型应为 t2.micro"
}
}
计划选项属性:
mode - normal(默认)或 refresh-onlyrefresh - 布尔值,默认为 truereplace - 要替换的资源地址列表target - 要定位的资源地址列表在测试文件级别(应用于所有运行块)或单个运行块内定义变量。
重要:测试文件中定义的变量具有最高优先级,覆盖环境变量、变量文件或命令行输入。
文件级变量:
# 应用于所有运行块
variables {
aws_region = "us-west-2"
instance_type = "t2.micro"
environment = "test"
}
run "test_with_file_variables" {
command = plan
assert {
condition = var.aws_region == "us-west-2"
error_message = "区域应为 us-west-2"
}
}
运行块变量(覆盖文件级变量):
variables {
instance_type = "t2.small"
environment = "test"
}
run "test_with_override_variables" {
command = plan
# 覆盖文件级变量
variables {
instance_type = "t3.large"
}
assert {
condition = var.instance_type == "t3.large"
error_message = "实例类型应覆盖为 t3.large"
}
}
引用先前运行块的变量:
run "setup_vpc" {
command = apply
}
run "test_with_vpc_output" {
command = plan
variables {
vpc_id = run.setup_vpc.vpc_id
}
assert {
condition = var.vpc_id == run.setup_vpc.vpc_id
error_message = "VPC ID 应与 setup_vpc 输出匹配"
}
}
断言块验证运行块内的条件。所有断言必须通过,测试才能成功。
语法:
assert {
condition = <表达式>
error_message = "失败描述"
}
资源属性断言:
run "test_resource_configuration" {
command = plan
assert {
condition = aws_s3_bucket.example.bucket == "my-test-bucket"
error_message = "存储桶名称应与预期值匹配"
}
assert {
condition = aws_s3_bucket.example.versioning[0].enabled == true
error_message = "存储桶版本控制应启用"
}
assert {
condition = length(aws_s3_bucket.example.tags) > 0
error_message = "存储桶应至少有一个标签"
}
}
输出验证:
run "test_outputs" {
command = plan
assert {
condition = output.vpc_id != ""
error_message = "VPC ID 输出不应为空"
}
assert {
condition = length(output.subnet_ids) == 3
error_message = "应恰好创建 3 个子网"
}
}
引用先前运行块的输出:
run "create_vpc" {
command = apply
}
run "validate_vpc_output" {
command = plan
assert {
condition = run.create_vpc.vpc_id != ""
error_message = "先前运行中的 VPC 应具有 ID"
}
}
复杂条件:
run "test_complex_validation" {
command = plan
assert {
condition = alltrue([
for subnet in aws_subnet.private :
can(regex("^10\\.0\\.", subnet.cidr_block))
])
error_message = "所有私有子网应使用 10.0.0.0/8 CIDR 范围"
}
assert {
condition = alltrue([
for instance in aws_instance.workers :
contains(["t2.micro", "t2.small", "t3.micro"], instance.instance_type)
])
error_message = "工作实例应使用已批准的实例类型"
}
}
测试某些条件是否故意失败。如果指定的可检查对象报告问题,则测试通过;如果它们没有报告问题,则测试失败。
可检查对象包括:输入变量、输出值、检查块以及托管资源或数据源。
run "test_invalid_input_rejected" {
command = plan
variables {
instance_count = -1
}
expect_failures = [
var.instance_count
]
}
测试自定义条件:
run "test_custom_condition_failure" {
command = plan
variables {
instance_type = "t2.nano" # 无效类型
}
expect_failures = [
var.instance_type
]
}
测试特定模块而不是根配置。
支持的模块源:
./modules/vpc、../shared/networkingterraform-aws-modules/vpc/awsapp.terraform.io/org/module/provider不支持的模块源:
git::https://github.com/...https://example.com/module.zip模块块属性:
source - 模块源(本地路径或注册表地址)version - 版本约束(仅适用于注册表模块)测试本地模块:
run "test_vpc_module" {
command = plan
module {
source = "./modules/vpc"
}
variables {
cidr_block = "10.0.0.0/16"
name = "test-vpc"
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR 应与输入变量匹配"
}
}
测试公共注册表模块:
run "test_registry_module" {
command = plan
module {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
variables {
name = "test-vpc"
cidr = "10.0.0.0/16"
}
assert {
condition = output.vpc_id != ""
error_message = "应创建 VPC"
}
}
为测试覆盖或配置提供程序。自 Terraform 1.7.0 起,提供程序块可以引用测试变量和先前运行块的输出。
基本提供程序配置:
provider "aws" {
region = "us-west-2"
}
run "test_with_provider" {
command = plan
assert {
condition = aws_instance.example.availability_zone == "us-west-2a"
error_message = "实例应在 us-west-2 区域"
}
}
多个提供程序配置:
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 = "实例应在 us-east-1 区域"
}
}
使用测试变量的提供程序:
variables {
aws_region = "eu-west-1"
}
provider "aws" {
region = var.aws_region
}
state_key 属性控制运行块使用哪个状态文件。默认情况下:
module 块引用)都有自己的状态文件强制运行块共享状态:
run "create_vpc" {
command = apply
module {
source = "./modules/vpc"
}
state_key = "shared_state"
}
run "create_subnet" {
command = apply
module {
source = "./modules/subnet"
}
state_key = "shared_state" # 与 create_vpc 共享状态
}
运行块默认按顺序执行。使用 parallel = true 启用并行执行。
并行执行的要求:
parallel = true 属性run "test_module_a" {
command = plan
parallel = true
module {
source = "./modules/module-a"
}
assert {
condition = output.result != ""
error_message = "模块 A 应产生输出"
}
}
run "test_module_b" {
command = plan
parallel = true
module {
source = "./modules/module-b"
}
assert {
condition = output.result != ""
error_message = "模块 B 应产生输出"
}
}
# 这创建了一个同步点
run "test_integration" {
command = plan
# 等待上述并行运行完成
assert {
condition = output.combined != ""
error_message = "集成应正常工作"
}
}
模拟提供程序模拟提供程序行为而无需创建真实基础设施(自 Terraform 1.7.0 起可用)。
基本模拟提供程序:
mock_provider "aws" {
mock_resource "aws_instance" {
defaults = {
id = "i-1234567890abcdef0"
instance_type = "t2.micro"
ami = "ami-12345678"
}
}
mock_data "aws_ami" {
defaults = {
id = "ami-12345678"
}
}
}
run "test_with_mocks" {
command = plan
assert {
condition = aws_instance.example.id == "i-1234567890abcdef0"
error_message = "模拟实例 ID 应匹配"
}
}
具有自定义值的高级模拟:
mock_provider "aws" {
alias = "mocked"
mock_resource "aws_s3_bucket" {
defaults = {
id = "test-bucket-12345"
bucket = "test-bucket"
arn = "arn:aws:s3:::test-bucket"
}
}
mock_data "aws_availability_zones" {
defaults = {
names = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
}
}
run "test_with_mock_provider" {
command = plan
providers = {
aws = provider.aws.mocked
}
assert {
condition = length(data.aws_availability_zones.available.names) == 3
error_message = "应返回 3 个可用区"
}
}
运行所有测试:
terraform test
运行特定测试文件:
terraform test tests/defaults.tftest.hcl
运行并显示详细输出:
terraform test -verbose
在特定目录中运行测试:
terraform test -test-directory=integration-tests
按名称过滤测试:
terraform test -filter=test_vpc_configuration
运行测试但不清理(用于调试):
terraform test -no-cleanup
成功的测试输出:
tests/defaults.tftest.hcl... in progress
run "test_default_configuration"... pass
run "test_outputs"... pass
tests/defaults.tftest.hcl... tearing down
tests/defaults.tftest.hcl... pass
Success! 2 passed, 0 failed.
失败的测试输出:
tests/defaults.tftest.hcl... in progress
run "test_default_configuration"... fail
Error: Test assertion failed
Instance type should be t2.micro by default
Success! 0 passed, 1 failed.
以下示例演示了使用 command = plan 的常见单元测试模式。这些测试在不创建真实基础设施的情况下验证 Terraform 逻辑,使其快速且无成本。
run "test_module_outputs" {
command = plan
assert {
condition = output.vpc_id != null
error_message = "VPC ID 输出必须已定义"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID 应以 'vpc-' 开头"
}
assert {
condition = length(output.subnet_ids) >= 2
error_message = "应输出至少 2 个子网 ID"
}
}
run "test_resource_count" {
command = plan
variables {
instance_count = 3
}
assert {
condition = length(aws_instance.workers) == 3
error_message = "应恰好创建 3 个工作实例"
}
}
run "test_conditional_resource_created" {
command = plan
variables {
create_nat_gateway = true
}
assert {
condition = length(aws_nat_gateway.main) == 1
error_message = "启用时应创建 NAT 网关"
}
}
run "test_conditional_resource_not_created" {
command = plan
variables {
create_nat_gateway = false
}
assert {
condition = length(aws_nat_gateway.main) == 0
error_message = "禁用时不应创建 NAT 网关"
}
}
run "test_resource_tags" {
command = plan
variables {
common_tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "环境标签应正确设置"
}
assert {
condition = aws_instance.example.tags["ManagedBy"] == "Terraform"
error_message = "ManagedBy 标签应正确设置"
}
}
run "setup_vpc" {
# command 默认为 apply
variables {
vpc_cidr = "10.0.0.0/16"
}
assert {
condition = output.vpc_id != ""
error_message = "应创建 VPC"
}
}
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 = "子网应在 setup_vpc 的 VPC 中创建"
}
}
run "test_data_source_lookup" {
command = plan
assert {
condition = data.aws_ami.ubuntu.id != ""
error_message = "应找到有效的 Ubuntu AMI"
}
assert {
condition = can(regex("^ami-", data.aws_ami.ubuntu.id))
error_message = "AMI ID 应为正确格式"
}
}
# 在 variables.tf 中
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "环境必须是 dev、staging 或 prod"
}
}
# 在测试文件中
run "test_valid_environment" {
command = plan
variables {
environment = "staging"
}
assert {
condition = var.environment == "staging"
error_message = "应接受有效的环境"
}
}
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
对于创建真实基础设施的测试(默认行为,使用 command = apply):
run "integration_test_full_stack" {
# command 默认为 apply
variables {
environment = "integration-test"
vpc_cidr = "10.100.0.0/16"
}
assert {
condition = aws_vpc.main.id != ""
error_message = "应创建 VPC"
}
assert {
condition = length(aws_subnet.private) == 2
error_message = "应创建 2 个私有子网"
}
assert {
condition = aws_instance.bastion.public_ip != ""
error_message = "堡垒实例应具有公共 IP"
}
}
# 测试完成后自动清理
重要:资源在测试完成后按运行块顺序反向销毁。这对于具有依赖关系的配置至关重要。
示例:对于包含对象的 S3 存储桶,必须在删除前清空存储桶:
run "create_bucket_with_objects" {
command = apply
assert {
condition = aws_s3_bucket.example.id != ""
error_message = "应创建存储桶"
}
}
run "add_objects_to_bucket" {
command = apply
assert {
condition = length(aws_s3_object.files) > 0
error_message = "应添加对象"
}
}
# 清理按反向顺序发生:
# 1. 销毁对象(run "add_objects_to_bucket")
# 2. 销毁存储桶(run "create_bucket_with_objects")
禁用清理以进行调试:
terraform test -no-cleanup
*_unit_test.tftest.hcl - 快速,不创建资源*_integration_test.tftest.hcl - 创建真实资源defaults_unit_test.tftest.hcl、validation_unit_test.tftest.hcl、full_stack_integration_test.tftest.hclcommand = apply(使用真实资源进行集成测试)command = plan(快速,无真实资源)-no-cleanup 标志进行调试terraform test 以尽早发现问题expect_failures 测试无效输入和预期失败parallel = true 以加速测试执行run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "标签应正确刷新"
}
}
run "test_specific_resource" {
command = plan
plan_options {
target = [
aws_instance.example
]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "目标资源应被计划"
}
}
run "test_networking_module" {
command = plan
parallel = true
module {
source = "./modules/networking"
}
variables {
cidr_block = "10.0.0.0/16"
}
assert {
condition = output.vpc_id != ""
error_message = "应创建 VPC"
}
}
run "test_compute_module" {
command = plan
parallel = true
module {
source = "./modules/compute"
}
variables {
instance_type = "t2.micro"
}
assert {
condition = output.instance_id != ""
error_message = "应创建实例"
}
}
run "create_foundation" {
command = apply
state_key = "foundation"
assert {
condition = aws_vpc.main.id != ""
error_message = "应创建基础 VPC"
}
}
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 = "应用程序应使用基础 VPC"
}
}
问题:断言失败
解决方案:查看错误消息,检查实际值与预期值,验证变量输入。使用 -verbose 标志获取详细输出
问题:测试因缺少凭据而失败
解决方案:为测试配置提供程序凭据,或对单元测试使用模拟提供程序(自 v1.7.0 起可用)
问题:测试因缺少依赖关系而失败
解决方案:使用顺序运行块或创建设置运行以建立所需资源。请记住清理按反向顺序发生
问题:测试运行时间过长
解决方案:
command = plan 而不是 applyparallel = true问题:多个测试相互干扰
解决方案:
state_key 属性控制状态文件共享问题:测试因不支持的模块源而失败
解决方案:Terraform 测试文件仅支持本地和注册表模块。将 Git 或 HTTP 源转换为本地模块或使用注册表模块
完整的示例,测试一个 VPC 模块,演示单元测试(计划模式)和集成测试(应用模式):
# tests/vpc_module_unit_test.tftest.hcl
# 此文件包含使用 command = plan 的单元测试(快速,不创建资源)
variables {
environment = "test"
aws_region = "us-west-2"
}
# ============================================================================
# 单元测试(计划模式)- 在不创建资源的情况下验证逻辑
# ============================================================================
# 测试默认配置
run "test_defaults" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR 应与输入匹配"
}
assert {
condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "默认应启用 DNS 主机名"
}
assert {
condition = aws_vpc.main.tags["Name"] == "test-vpc"
error_message = "VPC 名称标签应与输入匹配"
}
}
# 测试子网创建
run "test_subnets" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.11.0/24"]
}
assert {
condition = length(aws_subnet.public) == 2
error_message = "应创建 2 个公共子网"
}
assert {
condition = length(aws_subnet.private) == 2
error_message = "应创建 2 个私有子网"
}
assert {
condition = alltrue([
for subnet in aws_subnet.private :
subnet.map_public_ip_on_launch == false
])
error_message = "私有子网不应分配公共 IP"
}
}
# 测试输出
run "test_outputs" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC ID 输出不应为空"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID 应具有正确格式"
}
assert {
condition = output.vpc_cidr == "10.0.0.0/16"
error_message = "VPC CIDR 输出应与输入匹配"
}
}
# 测试无效的 CIDR 块
run "test_invalid_cidr" {
command = plan
variables {
vpc_cidr = "invalid"
vpc_name = "test-vpc"
}
expect_failures = [
var.vpc_cidr
]
}
# tests/vpc_module_integration_test.tftest.hcl
# 此文件包含使用 command = apply 的集成测试(创建真实资源)
variables {
environment = "integration-test"
aws_region = "us-west-2"
}
# ============================================================================
# 集成测试(应用模式)- 创建并验证真实基础设施
# ============================================================================
# 创建真实 VPC 的集成测试
run "integration_test_vpc_creation" {
# command 默认为 apply - 创建真实的 AWS 资源!
variables {
vpc_cidr = "10.100.0.0/16"
vpc_name = "integration-test-vpc"
}
assert {
condition = aws_vpc.main.id != ""
error_message = "VPC 应创建有效的 ID"
}
assert {
condition = aws_vpc.main.state == "available"
error_message = "VPC 应处于可用状态"
}
}
# tests/vpc_module_mock_test.tftest.hcl
# 此文件演示模拟提供程序测试 - 最快选项,无需凭据
# ============================================================================
# 模拟测试(计划模式与模拟)- 无真实基础设施或 API 调用
# ============================================================================
# 模拟测试适用于:
# - 无需云成本测试复杂逻辑
# - 无需提供程序凭据运行测试
# - 本地开发中的快速反馈
# - 无需云访问的 CI/CD 流水线
# - 使用可预测的数据源结果进行测试
# 定义模拟提供程序以模拟 AWS 行为
mock_provider "aws" {
# 模拟 EC2 实例 - 返回这些值而不是创建真实资源
mock_resource "aws_instance" {
defaults = {
id = "i-1234567890abcdef0"
arn = "arn:aws:ec2:us-west-2:123456789012:instance/i-1234567890abcdef0"
instance_type = "t2.micro"
ami = "ami-12345678"
availability_zone = "us-west-2a"
subnet_id = "subnet-12345678"
vpc_security_group_ids = ["sg-12345678"]
associate_public_ip_address = true
public_ip = "203.0.113.1"
private_ip = "10.0.1.100"
tags = {}
}
}
# 模拟 VPC 资源
mock_resource "aws_vpc" {
defaults = {
id = "vpc-12345678"
arn = "arn:aws:ec2:us-west-2:123456789012:vpc/vpc-12345678"
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
instance_tenancy = "default"
tags = {}
}
}
# 模拟子网资源
mock_resource "aws_subnet" {
defaults = {
id = "subnet-12345678"
arn = "arn:aws:ec2:us-west-2:123456789012:subnet/subnet-12345678"
vpc_id = "vpc-12345678"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
map_public_ip_on_launch = false
tags = {}
}
}
# 模拟 S3 存储桶资源
mock_resource "aws_s3_bucket" {
defaults = {
id = "test-bucket-12345"
arn = "arn:aws:s3:::test-bucket-12345"
bucket = "test-bucket-12345"
bucket_domain_name = "test-bucket-12345.s3.amazonaws.com"
region = "us-west-2"
tags = {}
}
}
# 模拟数据源 - 对于测试查询现有基础设施的模块至关重要
mock_data "aws_ami" {
defaults = {
id = "ami-0c55b159cbfafe1f0"
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20210430"
architecture = "x86_64"
root_device_type = "ebs"
virtualization_type = "hvm"
owners = ["099720109477"]
}
}
mock_data "aws_availability_zones" {
defaults = {
names = ["us-west-2a", "us-west-2b", "us-west-2c"]
zone_ids = ["us
Terraform's built-in testing framework enables module authors to validate that configuration updates don't introduce breaking changes. Tests execute against temporary resources, protecting existing infrastructure and state files.
Test File : A .tftest.hcl or .tftest.json file containing test configuration and run blocks that validate your Terraform configuration.
Test Block : Optional configuration block that defines test-wide settings (available since Terraform 1.6.0).
Run Block : Defines a single test scenario with optional variables, provider configurations, and assertions. Each test file requires at least one run block.
Assert Block : Contains conditions that must evaluate to true for the test to pass. Failed assertions cause the test to fail.
Mock Provider : Simulates provider behavior without creating real infrastructure (available since Terraform 1.7.0).
Test Modes : Tests run in apply mode (default, creates real infrastructure) or plan mode (validates logic without creating resources).
Terraform test files use the .tftest.hcl or .tftest.json extension and are typically organized in a tests/ directory. Use clear naming conventions to distinguish between unit tests (plan mode) and integration tests (apply mode):
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── validation_unit_test.tftest.hcl # Unit test (plan mode)
├── edge_cases_unit_test.tftest.hcl # Unit test (plan mode)
└── full_stack_integration_test.tftest.hcl # Integration test (apply mode - creates real resources)
A test file contains:
test block (configuration settings)run blocks (test executions)variables block (input values)provider blocks (provider configuration)mock_provider blocks (mock provider data, since v1.7.0)Important : The order of variables and provider blocks doesn't matter. Terraform processes all values within these blocks at the beginning of the test operation.
The optional test block configures test-wide settings:
test {
parallel = true # Enable parallel execution for all run blocks (default: false)
}
Test Block Attributes:
parallel - Boolean, when set to true, enables parallel execution for all run blocks by default (default: false). Individual run blocks can override this setting.Each run block executes a command against your configuration. Run blocks execute sequentially by default.
Basic Integration Test (Apply Mode - Default):
run "test_instance_creation" {
command = apply
assert {
condition = aws_instance.example.id != ""
error_message = "Instance should be created with a valid ID"
}
assert {
condition = output.instance_public_ip != ""
error_message = "Instance should have a public IP"
}
}
Unit Test (Plan Mode):
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"
}
assert {
condition = aws_instance.example.tags["Environment"] == "test"
error_message = "Environment tag should be 'test'"
}
}
Run Block Attributes:
command - Either apply (default) or planplan_options - Configure plan behavior (see below)variables - Override test-level variable valuesmodule - Reference alternate modules for testingproviders - Customize provider availabilityassert - Validation conditions (multiple allowed)expect_failures - Specify expected validation failuresstate_key - Manage state file isolation (since v1.9.0)The plan_options block configures plan command behavior:
run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only # "normal" (default) or "refresh-only"
refresh = true # boolean, defaults to true
replace = [
aws_instance.example
]
target = [
aws_instance.example
]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Instance type should be t2.micro"
}
}
Plan Options Attributes:
mode - normal (default) or refresh-onlyrefresh - Boolean, defaults to truereplace - List of resource addresses to replacetarget - List of resource addresses to targetDefine variables at the test file level (applied to all run blocks) or within individual run blocks.
Important : Variables defined in test files take the highest precedence , overriding environment variables, variables files, or command-line input.
File-Level Variables:
# Applied to all run blocks
variables {
aws_region = "us-west-2"
instance_type = "t2.micro"
environment = "test"
}
run "test_with_file_variables" {
command = plan
assert {
condition = var.aws_region == "us-west-2"
error_message = "Region should be us-west-2"
}
}
Run Block Variables (Override File-Level):
variables {
instance_type = "t2.small"
environment = "test"
}
run "test_with_override_variables" {
command = plan
# Override file-level variables
variables {
instance_type = "t3.large"
}
assert {
condition = var.instance_type == "t3.large"
error_message = "Instance type should be overridden to t3.large"
}
}
Variables Referencing Prior Run Blocks:
run "setup_vpc" {
command = apply
}
run "test_with_vpc_output" {
command = plan
variables {
vpc_id = run.setup_vpc.vpc_id
}
assert {
condition = var.vpc_id == run.setup_vpc.vpc_id
error_message = "VPC ID should match setup_vpc output"
}
}
Assert blocks validate conditions within run blocks. All assertions must pass for the test to succeed.
Syntax:
assert {
condition = <expression>
error_message = "failure description"
}
Resource Attribute Assertions:
run "test_resource_configuration" {
command = plan
assert {
condition = aws_s3_bucket.example.bucket == "my-test-bucket"
error_message = "Bucket name should match expected value"
}
assert {
condition = aws_s3_bucket.example.versioning[0].enabled == true
error_message = "Bucket versioning should be enabled"
}
assert {
condition = length(aws_s3_bucket.example.tags) > 0
error_message = "Bucket should have at least one tag"
}
}
Output Validation:
run "test_outputs" {
command = plan
assert {
condition = output.vpc_id != ""
error_message = "VPC ID output should not be empty"
}
assert {
condition = length(output.subnet_ids) == 3
error_message = "Should create exactly 3 subnets"
}
}
Referencing Prior Run Block Outputs:
run "create_vpc" {
command = apply
}
run "validate_vpc_output" {
command = plan
assert {
condition = run.create_vpc.vpc_id != ""
error_message = "VPC from previous run should have an ID"
}
}
Complex Conditions:
run "test_complex_validation" {
command = plan
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"
}
assert {
condition = alltrue([
for instance in aws_instance.workers :
contains(["t2.micro", "t2.small", "t3.micro"], instance.instance_type)
])
error_message = "Worker instances should use approved instance types"
}
}
Test that certain conditions intentionally fail. The test passes if the specified checkable objects report an issue, and fails if they do not.
Checkable objects include : Input variables, output values, check blocks, and managed resources or data sources.
run "test_invalid_input_rejected" {
command = plan
variables {
instance_count = -1
}
expect_failures = [
var.instance_count
]
}
Testing Custom Conditions:
run "test_custom_condition_failure" {
command = plan
variables {
instance_type = "t2.nano" # Invalid type
}
expect_failures = [
var.instance_type
]
}
Test a specific module rather than the root configuration.
Supported Module Sources:
./modules/vpc, ../shared/networkingterraform-aws-modules/vpc/awsapp.terraform.io/org/module/providerUnsupported Module Sources:
git::https://github.com/...https://example.com/module.zipModule Block Attributes:
source - Module source (local path or registry address)version - Version constraint (only for registry modules)Testing Local Modules:
run "test_vpc_module" {
command = plan
module {
source = "./modules/vpc"
}
variables {
cidr_block = "10.0.0.0/16"
name = "test-vpc"
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR should match input variable"
}
}
Testing Public Registry Modules:
run "test_registry_module" {
command = plan
module {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
variables {
name = "test-vpc"
cidr = "10.0.0.0/16"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
Override or configure providers for tests. Since Terraform 1.7.0, provider blocks can reference test variables and prior run block outputs.
Basic Provider Configuration:
provider "aws" {
region = "us-west-2"
}
run "test_with_provider" {
command = plan
assert {
condition = aws_instance.example.availability_zone == "us-west-2a"
error_message = "Instance should be in us-west-2 region"
}
}
Multiple Provider Configurations:
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"
}
}
Provider with Test Variables:
variables {
aws_region = "eu-west-1"
}
provider "aws" {
region = var.aws_region
}
The state_key attribute controls which state file a run block uses. By default:
module block) gets its own state fileForce Run Blocks to Share State:
run "create_vpc" {
command = apply
module {
source = "./modules/vpc"
}
state_key = "shared_state"
}
run "create_subnet" {
command = apply
module {
source = "./modules/subnet"
}
state_key = "shared_state" # Shares state with create_vpc
}
Run blocks execute sequentially by default. Enable parallel execution with parallel = true.
Requirements for Parallel Execution:
No inter-run output references (run blocks cannot reference outputs from parallel runs)
Different state files (via different modules or state keys)
Explicit parallel = true attribute
run "test_module_a" { command = plan parallel = true
module { source = "./modules/module-a" }
assert { condition = output.result != "" error_message = "Module A should produce output" } }
run "test_module_b" { command = plan parallel = true
module { source = "./modules/module-b" }
assert { condition = output.result != "" error_message = "Module B should produce output" } }
run "test_integration" { command = plan
assert { condition = output.combined != "" error_message = "Integration should work" } }
Mock providers simulate provider behavior without creating real infrastructure (available since Terraform 1.7.0).
Basic Mock Provider:
mock_provider "aws" {
mock_resource "aws_instance" {
defaults = {
id = "i-1234567890abcdef0"
instance_type = "t2.micro"
ami = "ami-12345678"
}
}
mock_data "aws_ami" {
defaults = {
id = "ami-12345678"
}
}
}
run "test_with_mocks" {
command = plan
assert {
condition = aws_instance.example.id == "i-1234567890abcdef0"
error_message = "Mock instance ID should match"
}
}
Advanced Mock with Custom Values:
mock_provider "aws" {
alias = "mocked"
mock_resource "aws_s3_bucket" {
defaults = {
id = "test-bucket-12345"
bucket = "test-bucket"
arn = "arn:aws:s3:::test-bucket"
}
}
mock_data "aws_availability_zones" {
defaults = {
names = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
}
}
run "test_with_mock_provider" {
command = plan
providers = {
aws = provider.aws.mocked
}
assert {
condition = length(data.aws_availability_zones.available.names) == 3
error_message = "Should return 3 availability zones"
}
}
Run all tests:
terraform test
Run specific test file:
terraform test tests/defaults.tftest.hcl
Run with verbose output:
terraform test -verbose
Run tests in a specific directory:
terraform test -test-directory=integration-tests
Filter tests by name:
terraform test -filter=test_vpc_configuration
Run tests without cleanup (for debugging):
terraform test -no-cleanup
Successful test output:
tests/defaults.tftest.hcl... in progress
run "test_default_configuration"... pass
run "test_outputs"... pass
tests/defaults.tftest.hcl... tearing down
tests/defaults.tftest.hcl... pass
Success! 2 passed, 0 failed.
Failed test output:
tests/defaults.tftest.hcl... in progress
run "test_default_configuration"... fail
Error: Test assertion failed
Instance type should be t2.micro by default
Success! 0 passed, 1 failed.
The following examples demonstrate common unit test patterns using command = plan. These tests validate Terraform logic without creating real infrastructure, making them fast and cost-free.
run "test_module_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-'"
}
assert {
condition = length(output.subnet_ids) >= 2
error_message = "Should output at least 2 subnet IDs"
}
}
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_conditional_resource_created" {
command = plan
variables {
create_nat_gateway = true
}
assert {
condition = length(aws_nat_gateway.main) == 1
error_message = "NAT gateway should be created when enabled"
}
}
run "test_conditional_resource_not_created" {
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_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 "setup_vpc" {
# command defaults to apply
variables {
vpc_cidr = "10.0.0.0/16"
}
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 created in the VPC from setup_vpc"
}
}
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"
}
}
# In variables.tf
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod"
}
}
# In test file
run "test_valid_environment" {
command = plan
variables {
environment = "staging"
}
assert {
condition = var.environment == "staging"
error_message = "Valid environment should be accepted"
}
}
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
For tests that create real infrastructure (default behavior with command = apply):
run "integration_test_full_stack" {
# command defaults to apply
variables {
environment = "integration-test"
vpc_cidr = "10.100.0.0/16"
}
assert {
condition = aws_vpc.main.id != ""
error_message = "VPC should be created"
}
assert {
condition = length(aws_subnet.private) == 2
error_message = "Should create 2 private subnets"
}
assert {
condition = aws_instance.bastion.public_ip != ""
error_message = "Bastion instance should have a public IP"
}
}
# Cleanup happens automatically after test completes
Important : Resources are destroyed in reverse run block order after test completion. This is critical for configurations with dependencies.
Example : For S3 buckets containing objects, the bucket must be emptied before deletion:
run "create_bucket_with_objects" {
command = apply
assert {
condition = aws_s3_bucket.example.id != ""
error_message = "Bucket should be created"
}
}
run "add_objects_to_bucket" {
command = apply
assert {
condition = length(aws_s3_object.files) > 0
error_message = "Objects should be added"
}
}
# Cleanup occurs in reverse order:
# 1. Destroys objects (run "add_objects_to_bucket")
# 2. Destroys bucket (run "create_bucket_with_objects")
Disable Cleanup for Debugging:
terraform test -no-cleanup
Test Organization : Organize tests by type using clear naming conventions:
*_unit_test.tftest.hcl - fast, no resources created*_integration_test.tftest.hcl - creates real resourcesdefaults_unit_test.tftest.hcl, validation_unit_test.tftest.hcl, full_stack_integration_test.tftest.hclApply vs Plan :
command = apply (integration testing with real resources)command = plan for unit tests (fast, no real resources)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"
}
variables {
cidr_block = "10.0.0.0/16"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_compute_module" {
command = plan
parallel = true
module {
source = "./modules/compute"
}
variables {
instance_type = "t2.micro"
}
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" # Share state with 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"
}
}
Issue : Assertion failures
Solution : Review error messages, check actual vs expected values, verify variable inputs. Use -verbose flag for detailed output
Issue : Tests fail due to missing credentials
Solution : Configure provider credentials for testing, or use mock providers for unit tests (available since v1.7.0)
Issue : Tests fail due to missing dependencies
Solution : Use sequential run blocks or create setup runs to establish required resources. Remember cleanup happens in reverse order
Issue : Tests take too long to run
Solution :
command = plan instead of apply where possibleparallel = true for independent testsIssue : Multiple tests interfere with each other
Solution :
state_key attribute to control state file sharingIssue : Test fails with unsupported module source
Solution : Terraform test files only support local and registry modules. Convert Git or HTTP sources to local modules or use registry modules
Complete example testing a VPC module, demonstrating both unit tests (plan mode) and integration tests (apply mode):
# tests/vpc_module_unit_test.tftest.hcl
# This file contains unit tests using command = plan (fast, no resources created)
variables {
environment = "test"
aws_region = "us-west-2"
}
# ============================================================================
# UNIT TESTS (Plan Mode) - Validate logic without creating resources
# ============================================================================
# Test default configuration
run "test_defaults" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR should match input"
}
assert {
condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "DNS hostnames should be enabled by default"
}
assert {
condition = aws_vpc.main.tags["Name"] == "test-vpc"
error_message = "VPC name tag should match input"
}
}
# Test subnet creation
run "test_subnets" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.11.0/24"]
}
assert {
condition = length(aws_subnet.public) == 2
error_message = "Should create 2 public subnets"
}
assert {
condition = length(aws_subnet.private) == 2
error_message = "Should create 2 private subnets"
}
assert {
condition = alltrue([
for subnet in aws_subnet.private :
subnet.map_public_ip_on_launch == false
])
error_message = "Private subnets should not assign public IPs"
}
}
# Test outputs
run "test_outputs" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC ID output should not be empty"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID should have correct format"
}
assert {
condition = output.vpc_cidr == "10.0.0.0/16"
error_message = "VPC CIDR output should match input"
}
}
# Test invalid CIDR block
run "test_invalid_cidr" {
command = plan
variables {
vpc_cidr = "invalid"
vpc_name = "test-vpc"
}
expect_failures = [
var.vpc_cidr
]
}
# tests/vpc_module_integration_test.tftest.hcl
# This file contains integration tests using command = apply (creates real resources)
variables {
environment = "integration-test"
aws_region = "us-west-2"
}
# ============================================================================
# INTEGRATION TESTS (Apply Mode) - Creates and validates real infrastructure
# ============================================================================
# Integration test creating real VPC
run "integration_test_vpc_creation" {
# command defaults to apply - creates real AWS resources!
variables {
vpc_cidr = "10.100.0.0/16"
vpc_name = "integration-test-vpc"
}
assert {
condition = aws_vpc.main.id != ""
error_message = "VPC should be created with valid ID"
}
assert {
condition = aws_vpc.main.state == "available"
error_message = "VPC should be in available state"
}
}
# tests/vpc_module_mock_test.tftest.hcl
# This file demonstrates mock provider testing - fastest option, no credentials needed
# ============================================================================
# MOCK TESTS (Plan Mode with Mocks) - No real infrastructure or API calls
# ============================================================================
# Mock tests are ideal for:
# - Testing complex logic without cloud costs
# - Running tests without provider credentials
# - Fast feedback in local development
# - CI/CD pipelines without cloud access
# - Testing with predictable data source results
# Define mock provider to simulate AWS behavior
mock_provider "aws" {
# Mock EC2 instances - returns these values instead of creating real resources
mock_resource "aws_instance" {
defaults = {
id = "i-1234567890abcdef0"
arn = "arn:aws:ec2:us-west-2:123456789012:instance/i-1234567890abcdef0"
instance_type = "t2.micro"
ami = "ami-12345678"
availability_zone = "us-west-2a"
subnet_id = "subnet-12345678"
vpc_security_group_ids = ["sg-12345678"]
associate_public_ip_address = true
public_ip = "203.0.113.1"
private_ip = "10.0.1.100"
tags = {}
}
}
# Mock VPC resources
mock_resource "aws_vpc" {
defaults = {
id = "vpc-12345678"
arn = "arn:aws:ec2:us-west-2:123456789012:vpc/vpc-12345678"
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
instance_tenancy = "default"
tags = {}
}
}
# Mock subnet resources
mock_resource "aws_subnet" {
defaults = {
id = "subnet-12345678"
arn = "arn:aws:ec2:us-west-2:123456789012:subnet/subnet-12345678"
vpc_id = "vpc-12345678"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
map_public_ip_on_launch = false
tags = {}
}
}
# Mock S3 bucket resources
mock_resource "aws_s3_bucket" {
defaults = {
id = "test-bucket-12345"
arn = "arn:aws:s3:::test-bucket-12345"
bucket = "test-bucket-12345"
bucket_domain_name = "test-bucket-12345.s3.amazonaws.com"
region = "us-west-2"
tags = {}
}
}
# Mock data sources - critical for testing modules that query existing infrastructure
mock_data "aws_ami" {
defaults = {
id = "ami-0c55b159cbfafe1f0"
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20210430"
architecture = "x86_64"
root_device_type = "ebs"
virtualization_type = "hvm"
owners = ["099720109477"]
}
}
mock_data "aws_availability_zones" {
defaults = {
names = ["us-west-2a", "us-west-2b", "us-west-2c"]
zone_ids = ["usw2-az1", "usw2-az2", "usw2-az3"]
}
}
mock_data "aws_vpc" {
defaults = {
id = "vpc-12345678"
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
}
}
}
# Test 1: Validate resource configuration with mocked values
run "test_instance_with_mocks" {
command = plan # Mocks only work with plan mode
variables {
instance_type = "t2.micro"
ami_id = "ami-12345678"
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Instance type should match input variable"
}
assert {
condition = aws_instance.example.id == "i-1234567890abcdef0"
error_message = "Mock should return consistent instance ID"
}
assert {
condition = can(regex("^203\\.0\\.113\\.", aws_instance.example.public_ip))
error_message = "Mock public IP should be in TEST-NET-3 range"
}
}
# Test 2: Validate data source behavior with mocked results
run "test_data_source_with_mocks" {
command = plan
assert {
condition = data.aws_ami.ubuntu.id == "ami-0c55b159cbfafe1f0"
error_message = "Mock data source should return predictable AMI ID"
}
assert {
condition = length(data.aws_availability_zones.available.names) == 3
error_message = "Should return 3 mocked availability zones"
}
assert {
condition = contains(
data.aws_availability_zones.available.names,
"us-west-2a"
)
error_message = "Should include us-west-2a in mocked zones"
}
}
# Test 3: Validate complex logic with for_each and mocks
run "test_multiple_subnets_with_mocks" {
command = plan
variables {
subnet_cidrs = {
"public-a" = "10.0.1.0/24"
"public-b" = "10.0.2.0/24"
"private-a" = "10.0.10.0/24"
"private-b" = "10.0.11.0/24"
}
}
# Test that all subnets are created
assert {
condition = length(keys(aws_subnet.subnets)) == 4
error_message = "Should create 4 subnets from for_each map"
}
# Test that public subnets have correct naming
assert {
condition = alltrue([
for name, subnet in aws_subnet.subnets :
can(regex("^public-", name)) ? subnet.map_public_ip_on_launch == true : true
])
error_message = "Public subnets should map public IPs on launch"
}
# Test that all subnets belong to mocked VPC
assert {
condition = alltrue([
for subnet in aws_subnet.subnets :
subnet.vpc_id == "vpc-12345678"
])
error_message = "All subnets should belong to mocked VPC"
}
}
# Test 4: Validate output values with mocks
run "test_outputs_with_mocks" {
command = plan
assert {
condition = output.vpc_id == "vpc-12345678"
error_message = "VPC ID output should match mocked value"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID output should have correct format"
}
assert {
condition = output.instance_public_ip == "203.0.113.1"
error_message = "Instance public IP should match mock"
}
}
# Test 5: Test conditional logic with mocks
run "test_conditional_resources_with_mocks" {
command = plan
variables {
create_bastion = true
create_nat_gateway = false
}
assert {
condition = length(aws_instance.bastion) == 1
error_message = "Bastion should be created when enabled"
}
assert {
condition = length(aws_nat_gateway.nat) == 0
error_message = "NAT gateway should not be created when disabled"
}
}
# Test 6: Test tag propagation with mocks
run "test_tag_inheritance_with_mocks" {
command = plan
variables {
common_tags = {
Environment = "test"
ManagedBy = "Terraform"
Project = "MockTesting"
}
}
# Verify tags are properly merged with defaults
assert {
condition = alltrue([
for key in keys(var.common_tags) :
contains(keys(aws_instance.example.tags), key)
])
error_message = "All common tags should be present on instance"
}
assert {
condition = aws_instance.example.tags["Environment"] == "test"
error_message = "Environment tag should be set correctly"
}
}
# Test 7: Test validation rules with mocks (expect_failures)
run "test_invalid_cidr_with_mocks" {
command = plan
variables {
vpc_cidr = "192.168.0.0/8" # Invalid - should be /16 or /24
}
# Expect custom validation to fail
expect_failures = [
var.vpc_cidr
]
}
# Test 8: Sequential mock tests with state sharing
run "setup_vpc_with_mocks" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
vpc_name = "test-vpc"
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR should match input"
}
}
run "test_subnet_references_vpc_with_mocks" {
command = plan
variables {
vpc_id = run.setup_vpc_with_mocks.vpc_id
subnet_cidr = "10.0.1.0/24"
}
assert {
condition = aws_subnet.example.vpc_id == run.setup_vpc_with_mocks.vpc_id
error_message = "Subnet should reference VPC from previous run"
}
assert {
condition = aws_subnet.example.vpc_id == "vpc-12345678"
error_message = "VPC ID should match mocked value"
}
}
Key Benefits of Mock Testing:
Limitations of Mock Testing:
command = applyWhen to Use Mock Tests:
name: Terraform Tests
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
terraform-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.0
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Run Terraform Tests
run: terraform test -verbose
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
terraform-test:
image: hashicorp/terraform:1.9
stage: test
before_script:
- terraform init
script:
- terraform fmt -check -recursive
- terraform validate
- terraform test -verbose
only:
- merge_requests
- main
For more information:
Weekly Installs
1.3K
Repository
GitHub Stars
481
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
github-copilot1.0K
opencode1.0K
codex980
gemini-cli966
amp848
kimi-cli844
Azure 升级评估与自动化工具 - 轻松迁移 Functions 计划、托管层级和 SKU
59,200 周安装
parallel - Enable parallel execution when set to true (since v1.9.0)Meaningful Assertions : Write clear, specific assertion error messages that help diagnose failures
Test Isolation : Each run block should be independent when possible. Use sequential runs only when testing dependencies
Variable Coverage : Test different variable combinations to validate all code paths. Remember that test variables have the highest precedence
Mock Providers : Use mocks for external dependencies to speed up tests and reduce costs (requires Terraform 1.7.0+)
Cleanup : Integration tests automatically destroy resources in reverse order after completion. Use -no-cleanup flag for debugging
CI Integration : Run terraform test in CI/CD pipelines to catch issues early
Test Naming : Use descriptive names for run blocks that explain what scenario is being tested
Negative Testing : Test invalid inputs and expected failures using expect_failures
Module Support : Remember that test files only support local and registry modules, not Git or other sources
Parallel Execution : Use parallel = true for independent tests with different state files to speed up test execution