terraform-test
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTerraform Test
Terraform Test
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.
Terraform的内置测试框架允许模块作者验证配置更新不会引入破坏性变更。测试针对临时资源执行,可保护现有基础设施和状态文件。
Core Concepts
核心概念
Test File: A or file containing test configuration and run blocks that validate your Terraform configuration.
.tftest.hcl.tftest.jsonTest 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配置的run块。
.tftest.hcl.tftest.jsonTest Block:可选的配置块,用于定义测试范围的全局设置(自Terraform 1.6.0起可用)。
Run Block:定义单个测试场景,可包含可选变量、Provider配置和断言。每个测试文件至少需要一个run块。
Assert Block:包含测试通过必须满足的条件。断言失败会导致测试失败。
Mock Provider:模拟Provider行为,无需创建真实基础设施(自Terraform 1.7.0起可用)。
测试模式:测试以apply模式(默认,创建真实基础设施)或plan模式(验证逻辑但不创建资源)运行。
File Structure
文件结构
Terraform test files use the or extension and are typically organized in a directory. Use clear naming conventions to distinguish between unit tests (plan mode) and integration tests (apply mode):
.tftest.hcl.tftest.jsontests/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)Terraform测试文件使用或扩展名,通常组织在目录中。使用清晰的命名约定区分单元测试(plan模式)和集成测试(apply模式):
.tftest.hcl.tftest.jsontests/my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── validation_unit_test.tftest.hcl # 单元测试(plan模式)
├── edge_cases_unit_test.tftest.hcl # 单元测试(plan模式)
└── full_stack_integration_test.tftest.hcl # 集成测试(apply模式 - 创建真实资源)Test File Components
测试文件组件
A test file contains:
- Zero to one block (configuration settings)
test - One to many blocks (test executions)
run - Zero to one block (input values)
variables - Zero to many blocks (provider configuration)
provider - Zero to many blocks (mock provider data, since v1.7.0)
mock_provider
Important: The order of and blocks doesn't matter. Terraform processes all values within these blocks at the beginning of the test operation.
variablesprovider测试文件包含:
- 0到1个 块(配置设置)
test - 1到多个 块(测试执行)
run - 0到1个 块(输入值)
variables - 0到多个 块(Provider配置)
provider - 0到多个 块(模拟Provider数据,自v1.7.0起可用)
mock_provider
重要提示:和块的顺序无关紧要。Terraform会在测试操作开始时处理这些块内的所有值。
variablesproviderTest Configuration (.tftest.hcl)
测试配置(.tftest.hcl)
Test Block
Test Block
The optional block configures test-wide settings:
testhcl
test {
parallel = true # Enable parallel execution for all run blocks (default: false)
}Test Block Attributes:
- - Boolean, when set to
parallel, enables parallel execution for all run blocks by default (default:true). Individual run blocks can override this setting.false
可选的块用于配置测试范围的全局设置:
testhcl
test {
parallel = true # 为所有run块启用并行执行(默认:false)
}Test Block属性:
- - 布尔值,设置为
parallel时,默认对所有run块启用并行执行(默认值:true)。单个run块可覆盖此设置。false
Run Block
Run Block
Each block executes a command against your configuration. Run blocks execute sequentially by default.
runBasic Integration Test (Apply Mode - Default):
hcl
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):
hcl
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:
- - Either
command(default) orapplyplan - - Configure plan behavior (see below)
plan_options - - Override test-level variable values
variables - - Reference alternate modules for testing
module - - Customize provider availability
providers - - Validation conditions (multiple allowed)
assert - - Specify expected validation failures
expect_failures - - Manage state file isolation (since v1.9.0)
state_key - - Enable parallel execution when set to
parallel(since v1.9.0)true
每个块针对配置执行一个命令。默认情况下,run块按顺序执行。
run基础集成测试(Apply模式 - 默认):
hcl
run "test_instance_creation" {
command = apply
assert {
condition = aws_instance.example.id != ""
error_message = "实例应创建有效的ID"
}
assert {
condition = output.instance_public_ip != ""
error_message = "实例应具有公网IP"
}
}单元测试(Plan模式):
hcl
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 = "Environment标签应为'test'"
}
}Run Block属性:
- - 可选值为
command(默认)或applyplan - - 配置plan行为(见下文)
plan_options - - 覆盖测试级别的变量值
variables - - 引用用于测试的替代模块
module - - 自定义Provider可用性
providers - - 验证条件(允许多个)
assert - - 指定预期的验证失败
expect_failures - - 管理状态文件隔离(自v1.9.0起可用)
state_key - - 设置为
parallel时启用并行执行(自v1.9.0起可用)true
Plan Options
Plan Options
The block configures plan command behavior:
plan_optionshcl
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(default) ornormalrefresh-only - - Boolean, defaults to
refreshtrue - - List of resource addresses to replace
replace - - List of resource addresses to target
target
plan_optionshcl
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"
}
}Plan Options属性:
- -
mode(默认)或normalrefresh-only - - 布尔值,默认值为
refreshtrue - - 要替换的资源地址列表
replace - - 要定位的资源地址列表
target
Variables Block
Variables Block
Define 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:
hcl
undefined可在测试文件级别(应用于所有run块)或单个run块内定义变量。
重要提示:测试文件中定义的变量优先级最高,会覆盖环境变量、变量文件或命令行输入的值。
文件级变量:
hcl
undefinedApplied to all run blocks
应用于所有run块
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):**
```hcl
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:
hcl
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"
}
}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"
}
}
**Run块变量(覆盖文件级变量):**
```hcl
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块的变量:
hcl
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 Block
Assert Block
Assert blocks validate conditions within run blocks. All assertions must pass for the test to succeed.
Syntax:
hcl
assert {
condition = <expression>
error_message = "failure description"
}Resource Attribute Assertions:
hcl
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:
hcl
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:
hcl
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:
hcl
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"
}
}Assert块用于验证run块内的条件。所有断言必须通过,测试才能成功。
语法:
hcl
assert {
condition = <表达式>
error_message = "失败描述"
}资源属性断言:
hcl
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 = "存储桶应至少有一个标签"
}
}输出验证:
hcl
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块的输出:
hcl
run "create_vpc" {
command = apply
}
run "validate_vpc_output" {
command = plan
assert {
condition = run.create_vpc.vpc_id != ""
error_message = "之前run创建的VPC应具有ID"
}
}复杂条件:
hcl
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 = "工作节点实例应使用批准的实例类型"
}
}Expect Failures Block
Expect Failures Block
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.
hcl
run "test_invalid_input_rejected" {
command = plan
variables {
instance_count = -1
}
expect_failures = [
var.instance_count
]
}Testing Custom Conditions:
hcl
run "test_custom_condition_failure" {
command = plan
variables {
instance_type = "t2.nano" # Invalid type
}
expect_failures = [
var.instance_type
]
}测试某些条件是否会故意失败。如果指定的可检查对象报告问题,则测试通过;如果没有报告问题,则测试失败。
可检查对象包括:输入变量、输出值、check块、托管资源或数据源。
hcl
run "test_invalid_input_rejected" {
command = plan
variables {
instance_count = -1
}
expect_failures = [
var.instance_count
]
}测试自定义条件:
hcl
run "test_custom_condition_failure" {
command = plan
variables {
instance_type = "t2.nano" # 无效类型
}
expect_failures = [
var.instance_type
]
}Module Block
Module Block
Test a specific module rather than the root configuration.
Supported Module Sources:
- ✅ Local modules: ,
./modules/vpc../shared/networking - ✅ Public Terraform Registry:
terraform-aws-modules/vpc/aws - ✅ Private Registry (HCP Terraform):
app.terraform.io/org/module/provider
Unsupported Module Sources:
- ❌ Git repositories:
git::https://github.com/... - ❌ HTTP URLs:
https://example.com/module.zip - ❌ Other remote sources (S3, GCS, etc.)
Module Block Attributes:
- - Module source (local path or registry address)
source - - Version constraint (only for registry modules)
version
Testing Local Modules:
hcl
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:
hcl
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"
}
}测试特定模块而非根配置。
支持的模块源:
- ✅ 本地模块:,
./modules/vpc../shared/networking - ✅ 公共Terraform Registry:
terraform-aws-modules/vpc/aws - ✅ 私有Registry(HCP Terraform):
app.terraform.io/org/module/provider
不支持的模块源:
- ❌ Git仓库:
git::https://github.com/... - ❌ HTTP URL:
https://example.com/module.zip - ❌ 其他远程源(S3、GCS等)
Module Block属性:
- - 模块源(本地路径或Registry地址)
source - - 版本约束(仅适用于Registry模块)
version
测试本地模块:
hcl
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应与输入变量匹配"
}
}测试公共Registry模块:
hcl
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"
}
}Provider Configuration
Provider配置
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:
hcl
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:
hcl
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:
hcl
variables {
aws_region = "eu-west-1"
}
provider "aws" {
region = var.aws_region
}为测试覆盖或配置Provider。自Terraform 1.7.0起,Provider块可引用测试变量和之前run块的输出。
基础Provider配置:
hcl
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配置:
hcl
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区域"
}
}带测试变量的Provider:
hcl
variables {
aws_region = "eu-west-1"
}
provider "aws" {
region = var.aws_region
}State Key Management
状态键管理
The attribute controls which state file a run block uses. By default:
state_key- The main configuration shares a state file across all run blocks
- Each alternate module (referenced via block) gets its own state file
module
Force Run Blocks to Share State:
hcl
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
}state_key- 主配置在所有run块之间共享一个状态文件
- 每个替代模块(通过块引用)有自己的状态文件
module
强制run块共享状态:
hcl
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 Execution
并行执行
Run blocks execute sequentially by default. Enable parallel execution with .
parallel = trueRequirements 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 attribute
parallel = true
hcl
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块按顺序执行。使用启用并行执行。
parallel = true并行执行要求:
- 无跨run输出引用(run块不能引用并行运行的输出)
- 不同的状态文件(通过不同模块或状态键)
- 显式设置属性
parallel = true
hcl
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应生成输出"
}
}This creates a synchronization point
此处创建同步点
run "test_integration" {
command = plan
Waits for parallel runs above to complete
assert {
condition = output.combined != ""
error_message = "Integration should work"
}
}
undefinedrun "test_integration" {
command = plan
等待上面的并行运行完成
assert {
condition = output.combined != ""
error_message = "集成应正常工作"
}
}
undefinedMock Providers
Mock Providers
Mock providers simulate provider behavior without creating real infrastructure (available since Terraform 1.7.0).
Basic Mock Provider:
hcl
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:
hcl
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"
}
}Mock Provider模拟Provider行为,无需创建真实基础设施(自Terraform 1.7.0起可用)。
基础Mock Provider:
hcl
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实例ID应匹配"
}
}带自定义值的高级Mock:
hcl
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个可用区"
}
}Test Execution
测试执行
Running Tests
运行测试
Run all tests:
bash
terraform testRun specific test file:
bash
terraform test tests/defaults.tftest.hclRun with verbose output:
bash
terraform test -verboseRun tests in a specific directory:
bash
terraform test -test-directory=integration-testsFilter tests by name:
bash
terraform test -filter=test_vpc_configurationRun tests without cleanup (for debugging):
bash
terraform test -no-cleanup运行所有测试:
bash
terraform test运行特定测试文件:
bash
terraform test tests/defaults.tftest.hcl带详细输出运行:
bash
terraform test -verbose运行特定目录中的测试:
bash
terraform test -test-directory=integration-tests按名称过滤测试:
bash
terraform test -filter=test_vpc_configuration运行测试不清理资源(用于调试):
bash
terraform test -no-cleanupTest Output
测试输出
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.成功测试输出:
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.Common Test Patterns (Unit Tests - Plan Mode)
常见测试模式(单元测试 - Plan模式)
The following examples demonstrate common unit test patterns using . These tests validate Terraform logic without creating real infrastructure, making them fast and cost-free.
command = plan以下示例展示了使用的常见单元测试模式。这些测试无需创建真实基础设施即可验证Terraform逻辑,速度快且无成本。
command = planTesting Module Outputs
测试模块输出
hcl
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"
}
}hcl
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"
}
}Testing Resource Counts
测试资源计数
hcl
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"
}
}hcl
run "test_resource_count" {
command = plan
variables {
instance_count = 3
}
assert {
condition = length(aws_instance.workers) == 3
error_message = "应创建恰好3个工作节点实例"
}
}Testing Conditional Resources
测试条件资源
hcl
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"
}
}hcl
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网关"
}
}Testing Tags
测试标签
hcl
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"
}
}hcl
run "test_resource_tags" {
command = plan
variables {
common_tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Environment标签应正确设置"
}
assert {
condition = aws_instance.example.tags["ManagedBy"] == "Terraform"
error_message = "ManagedBy标签应正确设置"
}
}Sequential Tests with Dependencies
带依赖的顺序测试
hcl
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"
}
}hcl
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中创建"
}
}Testing Data Sources
测试数据源
hcl
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"
}
}hcl
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格式应正确"
}
}Testing Validation Rules
测试验证规则
hcl
undefinedhcl
undefinedIn variables.tf
在variables.tf中
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod"
}
}
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment必须是dev、staging或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
]
}
undefinedrun "test_valid_environment" {
command = plan
variables {
environment = "staging"
}
assert {
condition = var.environment == "staging"
error_message = "有效的Environment应被接受"
}
}
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
undefinedIntegration Testing
集成测试
For tests that create real infrastructure (default behavior with ):
command = applyhcl
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"
}
}对于创建真实基础设施的测试(的默认行为):
command = applyhcl
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"
}
}Cleanup happens automatically after test completes
测试完成后自动清理
undefinedundefinedCleanup and Destruction
清理与销毁
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:
hcl
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"
}
}重要提示:测试完成后,资源会按run块的逆序销毁。这对于有依赖关系的配置至关重要。
示例:对于包含对象的S3存储桶,必须先清空存储桶才能删除:
hcl
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 = "应添加对象"
}
}Cleanup occurs in reverse order:
清理按逆序进行:
1. Destroys objects (run "add_objects_to_bucket")
1. 销毁对象(run "add_objects_to_bucket")
2. Destroys bucket (run "create_bucket_with_objects")
2. 销毁存储桶(run "create_bucket_with_objects")
**Disable Cleanup for Debugging:**
```bash
terraform test -no-cleanup
**禁用清理以进行调试:**
```bash
terraform test -no-cleanupBest Practices
最佳实践
-
Test Organization: Organize tests by type using clear naming conventions:
- Unit tests (plan mode): - fast, no resources created
*_unit_test.tftest.hcl - Integration tests (apply mode): - creates real resources
*_integration_test.tftest.hcl - Example: ,
defaults_unit_test.tftest.hcl,validation_unit_test.tftest.hclfull_stack_integration_test.tftest.hcl - This makes it easy to run unit tests separately from integration tests in CI/CD
- Unit tests (plan mode):
-
Apply vs Plan:
- Default is (integration testing with real resources)
command = apply - Use for unit tests (fast, no real resources)
command = plan - Use mocks for isolated unit testing
- Default is
-
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. Useflag for debugging
-no-cleanup -
CI Integration: Runin CI/CD pipelines to catch issues early
terraform test -
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: Usefor independent tests with different state files to speed up test execution
parallel = true
-
测试组织:使用清晰的命名约定按类型组织测试:
- 单元测试(plan模式):- 速度快,不创建资源
*_unit_test.tftest.hcl - 集成测试(apply模式):- 创建真实资源
*_integration_test.tftest.hcl - 示例:,
defaults_unit_test.tftest.hcl,validation_unit_test.tftest.hclfull_stack_integration_test.tftest.hcl - 这使得在CI/CD中单独运行单元测试和集成测试变得容易
- 单元测试(plan模式):
-
Apply vs Plan:
- 默认是(使用真实资源的集成测试)
command = apply - 对单元测试使用(速度快,无真实资源)
command = plan - 对隔离的单元测试使用mocks
- 默认是
-
有意义的断言:编写清晰、具体的断言错误消息,帮助诊断失败
-
测试隔离:尽可能让每个run块独立。仅在测试依赖关系时使用顺序运行
-
变量覆盖:测试不同的变量组合以验证所有代码路径。记住测试变量优先级最高
-
Mock Providers:对外部依赖使用mocks以加快测试速度并降低成本(需要Terraform 1.7.0+)
-
清理:集成测试完成后会自动按逆序销毁资源。使用标志进行调试
-no-cleanup -
CI集成:在CI/CD流水线中运行以尽早发现问题
terraform test -
测试命名:为run块使用描述性名称,解释正在测试的场景
-
负面测试:使用测试无效输入和预期失败
expect_failures -
模块支持:记住测试文件仅支持本地和Registry模块,不支持Git或其他源
-
并行执行:对具有不同状态文件的独立测试使用以加快测试执行
parallel = true
Advanced Features
高级功能
Testing with Refresh-Only Mode
使用Refresh-Only模式测试
hcl
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"
}
}hcl
run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "标签应正确刷新"
}
}Testing with Targeted Resources
测试目标资源
hcl
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"
}
}hcl
run "test_specific_resource" {
command = plan
plan_options {
target = [
aws_instance.example
]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "应规划目标资源"
}
}Testing Multiple Modules in Parallel
并行测试多个模块
hcl
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"
}
}hcl
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 = "应创建实例"
}
}Custom State Management
自定义状态管理
hcl
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"
}
}hcl
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" # 与foundation共享状态
variables {
vpc_id = run.create_foundation.vpc_id
}
assert {
condition = aws_instance.app.vpc_id == run.create_foundation.vpc_id
error_message = "应用应使用基础VPC"
}
}Troubleshooting
故障排除
Test Failures
测试失败
Issue: Assertion failures
Solution: Review error messages, check actual vs expected values, verify variable inputs. Use flag for detailed output
-verbose问题:断言失败
解决方案:查看错误消息,检查实际值与预期值,验证变量输入。使用标志获取详细输出
-verboseProvider Authentication
Provider认证
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)
问题:测试因缺少凭证失败
解决方案:为测试配置Provider凭证,或对单元测试使用mock providers(自v1.7.0起可用)
Resource Dependencies
资源依赖
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
问题:测试因缺少依赖失败
解决方案:使用顺序run块或创建setup run来建立所需资源。记住清理按逆序进行
Long Test Execution
测试执行时间长
Issue: Tests take too long to run
Solution:
- Use instead of
command = planwhere possibleapply - Leverage mock providers
- Use for independent tests
parallel = true - Organize slow integration tests separately
问题:测试运行时间过长
解决方案:
- 尽可能使用而非
command = planapply - 利用mock providers
- 对独立测试使用
parallel = true - 将慢的集成测试单独组织
State Conflicts
状态冲突
Issue: Multiple tests interfere with each other
Solution:
- Use different modules (automatic separate state)
- Use attribute to control state file sharing
state_key - Use mock providers for isolated testing
问题:多个测试相互干扰
解决方案:
- 使用不同的模块(自动分离状态)
- 使用属性控制状态文件共享
state_key - 使用mock providers进行隔离测试
Module Source Errors
模块源错误
Issue: 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
问题:测试因不支持的模块源失败
解决方案:Terraform测试文件仅支持本地和Registry模块。将Git或HTTP源转换为本地模块或使用Registry模块
Example Test Suite
示例测试套件
Complete example testing a VPC module, demonstrating both unit tests (plan mode) and integration tests (apply mode):
hcl
undefined完整示例测试VPC模块,展示单元测试(plan模式)和集成测试(apply模式):
hcl
undefinedtests/vpc_module_unit_test.tftest.hcl
tests/vpc_module_unit_test.tftest.hcl
This file contains unit tests using command = plan (fast, no resources created)
此文件包含使用command = plan的单元测试(速度快,不创建资源)
variables {
environment = "test"
aws_region = "us-west-2"
}
variables {
environment = "test"
aws_region = "us-west-2"
}
============================================================================
============================================================================
UNIT TESTS (Plan Mode) - Validate logic without creating resources
单元测试(Plan模式)- 无需创建资源即可验证逻辑
============================================================================
============================================================================
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"
}
}
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名称标签应与输入匹配"
}
}
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"
}
}
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"
}
}
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"
}
}
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输出应与输入匹配"
}
}
Test invalid CIDR block
测试无效CIDR块
run "test_invalid_cidr" {
command = plan
variables {
vpc_cidr = "invalid"
vpc_name = "test-vpc"
}
expect_failures = [
var.vpc_cidr
]
}
```hclrun "test_invalid_cidr" {
command = plan
variables {
vpc_cidr = "invalid"
vpc_name = "test-vpc"
}
expect_failures = [
var.vpc_cidr
]
}
```hcltests/vpc_module_integration_test.tftest.hcl
tests/vpc_module_integration_test.tftest.hcl
This file contains integration tests using command = apply (creates real resources)
此文件包含使用command = apply的集成测试(创建真实资源)
variables {
environment = "integration-test"
aws_region = "us-west-2"
}
variables {
environment = "integration-test"
aws_region = "us-west-2"
}
============================================================================
============================================================================
INTEGRATION TESTS (Apply Mode) - Creates and validates real infrastructure
集成测试(Apply模式)- 创建并验证真实基础设施
============================================================================
============================================================================
Integration test creating real VPC
创建真实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"
}
}
```hclrun "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 = "应创建具有有效ID的VPC"
}
assert {
condition = aws_vpc.main.state == "available"
error_message = "VPC应处于可用状态"
}
}
```hcltests/vpc_module_mock_test.tftest.hcl
tests/vpc_module_mock_test.tftest.hcl
This file demonstrates mock provider testing - fastest option, no credentials needed
此文件展示mock provider测试 - 速度最快,无需凭证
============================================================================
============================================================================
MOCK TESTS (Plan Mode with Mocks) - No real infrastructure or API calls
Mock测试(带Mocks的Plan模式)- 无真实基础设施或API调用
============================================================================
============================================================================
Mock tests are ideal for:
Mock测试适用于:
- Testing complex logic without cloud costs
- 测试Terraform逻辑和条件
- Running tests without provider credentials
- 验证变量转换
- Fast feedback in local development
- 测试for_each和count表达式
- CI/CD pipelines without cloud access
- 检查输出计算
- Testing with predictable data source results
- 无云访问的本地开发
Define mock provider to simulate AWS behavior
- 快速CI/CD反馈循环
—
- 验证实际Provider行为
—
- 测试真实资源创建的副作用
—
- 验证API级交互
—
- 端到端集成测试
—
定义mock provider以模拟AWS行为
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
}
}
}
mock_provider "aws" {
Mock 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 = {}
}
}
Mock 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子网资源
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存储桶资源
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数据源 - 对于查询现有基础设施的模块测试至关重要
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
测试1:使用mock值验证资源配置
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"
}
}
run "test_instance_with_mocks" {
command = plan # Mocks仅在plan模式下工作
variables {
instance_type = "t2.micro"
ami_id = "ami-12345678"
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "实例类型应与输入变量匹配"
}
assert {
condition = aws_instance.example.id == "i-1234567890abcdef0"
error_message = "Mock应返回一致的实例ID"
}
assert {
condition = can(regex("^203\.0\.113\.", aws_instance.example.public_ip))
error_message = "Mock公网IP应属于TEST-NET-3范围"
}
}
Test 2: Validate data source behavior with mocked results
测试2:使用mock结果验证数据源行为
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"
}
}
run "test_data_source_with_mocks" {
command = plan
assert {
condition = data.aws_ami.ubuntu.id == "ami-0c55b159cbfafe1f0"
error_message = "Mock数据源应返回可预测的AMI ID"
}
assert {
condition = length(data.aws_availability_zones.available.names) == 3
error_message = "应返回3个mock可用区"
}
assert {
condition = contains(
data.aws_availability_zones.available.names,
"us-west-2a"
)
error_message = "应在mock可用区中包含us-west-2a"
}
}
Test 3: Validate complex logic with for_each and mocks
测试3:使用mocks验证for_each的复杂逻辑
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"
}
}
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"
}
}
测试是否创建了所有子网
assert {
condition = length(keys(aws_subnet.subnets)) == 4
error_message = "应从for_each映射创建4个子网"
}
测试公有子网的命名是否正确
assert {
condition = alltrue([
for name, subnet in aws_subnet.subnets :
can(regex("^public-", name)) ? subnet.map_public_ip_on_launch == true : true
])
error_message = "公有子网应在启动时映射公网IP"
}
测试所有子网是否属于mock VPC
assert {
condition = alltrue([
for subnet in aws_subnet.subnets :
subnet.vpc_id == "vpc-12345678"
])
error_message = "所有子网应属于mock VPC"
}
}
Test 4: Validate output values with mocks
测试4:使用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"
}
}
run "test_outputs_with_mocks" {
command = plan
assert {
condition = output.vpc_id == "vpc-12345678"
error_message = "VPC ID输出应与mock值匹配"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID输出格式应正确"
}
assert {
condition = output.instance_public_ip == "203.0.113.1"
error_message = "实例公网IP应与mock匹配"
}
}
Test 5: Test conditional logic with mocks
测试5:使用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"
}
}
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 = "启用时应创建堡垒机"
}
assert {
condition = length(aws_nat_gateway.nat) == 0
error_message = "禁用时不应创建NAT网关"
}
}
Test 6: Test tag propagation with mocks
测试6:使用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"
}
}
run "test_tag_inheritance_with_mocks" {
command = plan
variables {
common_tags = {
Environment = "test"
ManagedBy = "Terraform"
Project = "MockTesting"
}
}
验证标签是否与默认值正确合并
assert {
condition = alltrue([
for key in keys(var.common_tags) :
contains(keys(aws_instance.example.tags), key)
])
error_message = "所有公共标签应出现在实例上"
}
assert {
condition = aws_instance.example.tags["Environment"] == "test"
error_message = "Environment标签应正确设置"
}
}
Test 7: Test validation rules with mocks (expect_failures)
测试7:使用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
]
}
run "test_invalid_cidr_with_mocks" {
command = plan
variables {
vpc_cidr = "192.168.0.0/8" # 无效 - 应为/16或/24
}
预期自定义验证失败
expect_failures = [
var.vpc_cidr
]
}
Test 8: Sequential mock tests with state sharing
测试8:带状态共享的顺序mock测试
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:**
1. **No Cloud Costs**: Runs entirely locally without creating infrastructure
2. **No Credentials Needed**: Perfect for CI/CD environments without cloud access
3. **Fast Execution**: Tests complete in seconds, not minutes
4. **Predictable Results**: Data sources return consistent values
5. **Isolated Testing**: No dependencies on existing cloud resources
6. **Safe Experimentation**: Test destructive operations without risk
**Limitations of Mock Testing:**
1. **Plan Mode Only**: Mocks don't work with `command = apply`
2. **Not Real Behavior**: Mocks may not reflect actual provider API behavior
3. **Computed Values**: Mock defaults may not match real computed attributes
4. **Provider Updates**: Mocks need manual updates when provider schemas change
5. **Resource Interactions**: Can't test real resource dependencies or timing issues
**When to Use Mock Tests:**
- ✅ Testing Terraform logic and conditionals
- ✅ Validating variable transformations
- ✅ Testing for_each and count expressions
- ✅ Checking output calculations
- ✅ Local development without cloud access
- ✅ Fast CI/CD feedback loops
- ❌ Validating actual provider behavior
- ❌ Testing real resource creation side effects
- ❌ Verifying API-level interactions
- ❌ End-to-end integration testingrun "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应与输入匹配"
}
}
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 = "子网应引用之前run的VPC"
}
assert {
condition = aws_subnet.example.vpc_id == "vpc-12345678"
error_message = "VPC ID应与mock值匹配"
}
}
**Mock测试的主要优势:**
1. **无云成本**:完全在本地运行,无需创建基础设施
2. **无需凭证**:非常适合无云访问的CI/CD环境
3. **执行速度快**:测试在数秒内完成,而非数分钟
4. **结果可预测**:数据源返回一致的值
5. **测试隔离**:不依赖现有云资源
6. **安全实验**:测试破坏性操作无风险
**Mock测试的局限性:**
1. **仅Plan模式**:Mocks不适用于`command = apply`
2. **非真实行为**:Mocks可能无法反映实际Provider API行为
3. **计算值**:Mock默认值可能与真实计算属性不匹配
4. **Provider更新**:Provider schema变更时,Mocks需要手动更新
5. **资源交互**:无法测试真实资源依赖或时序问题
**何时使用Mock测试:**
- ✅ 测试Terraform逻辑和条件
- ✅ 验证变量转换
- ✅ 测试for_each和count表达式
- ✅ 检查输出计算
- ✅ 无云访问的本地开发
- ✅ 快速CI/CD反馈循环
- ❌ 验证实际Provider行为
- ❌ 测试真实资源创建的副作用
- ❌ 验证API级交互
- ❌ 端到端集成测试CI/CD Integration
CI/CD集成
GitHub Actions Example
GitHub Actions示例
yaml
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 }}yaml
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 }}GitLab CI Example
GitLab CI示例
yaml
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
- mainyaml
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
- mainReferences
参考资料
For more information: