terraform-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Terraform 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
.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).
测试文件:扩展名为
.tftest.hcl
.tftest.json
的文件,包含测试配置和用于验证Terraform配置的run块。
Test 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
.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)
Terraform测试文件使用
.tftest.hcl
.tftest.json
扩展名,通常组织在
tests/
目录中。使用清晰的命名约定区分单元测试(plan模式)和集成测试(apply模式):
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
    test
    block (configuration settings)
  • One to many
    run
    blocks (test executions)
  • Zero to one
    variables
    block (input values)
  • Zero to many
    provider
    blocks (provider configuration)
  • Zero to many
    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.
测试文件包含:
  • 0到1个
    test
    块(配置设置)
  • 1到多个
    run
    块(测试执行)
  • 0到1个
    variables
    块(输入值)
  • 0到多个
    provider
    块(Provider配置)
  • 0到多个
    mock_provider
    块(模拟Provider数据,自v1.7.0起可用)
重要提示
variables
provider
块的顺序无关紧要。Terraform会在测试操作开始时处理这些块内的所有值。

Test Configuration (.tftest.hcl)

测试配置(.tftest.hcl)

Test Block

Test Block

The optional
test
block configures test-wide settings:
hcl
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.
可选的
test
块用于配置测试范围的全局设置:
hcl
test {
  parallel = true  # 为所有run块启用并行执行(默认:false)
}
Test Block属性:
  • parallel
    - 布尔值,设置为
    true
    时,默认对所有run块启用并行执行(默认值:
    false
    )。单个run块可覆盖此设置。

Run Block

Run Block

Each
run
block executes a command against your configuration. Run blocks execute sequentially by default.
Basic 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:
  • command
    - Either
    apply
    (default) or
    plan
  • plan_options
    - Configure plan behavior (see below)
  • variables
    - Override test-level variable values
  • module
    - Reference alternate modules for testing
  • providers
    - Customize provider availability
  • assert
    - Validation conditions (multiple allowed)
  • expect_failures
    - Specify expected validation failures
  • state_key
    - Manage state file isolation (since v1.9.0)
  • parallel
    - Enable parallel execution when set to
    true
    (since v1.9.0)
每个
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
    - 可选值为
    apply
    (默认)或
    plan
  • plan_options
    - 配置plan行为(见下文)
  • variables
    - 覆盖测试级别的变量值
  • module
    - 引用用于测试的替代模块
  • providers
    - 自定义Provider可用性
  • assert
    - 验证条件(允许多个)
  • expect_failures
    - 指定预期的验证失败
  • state_key
    - 管理状态文件隔离(自v1.9.0起可用)
  • parallel
    - 设置为
    true
    时启用并行执行(自v1.9.0起可用)

Plan Options

Plan Options

The
plan_options
block configures plan command behavior:
hcl
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-only
  • refresh
    - Boolean, defaults to
    true
  • replace
    - List of resource addresses to replace
  • target
    - List of resource addresses to target
plan_options
块用于配置plan命令的行为:
hcl
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
    -
    normal
    (默认)或
    refresh-only
  • refresh
    - 布尔值,默认值为
    true
  • 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
undefined

Applied 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:
  • source
    - Module source (local path or registry address)
  • version
    - Version constraint (only for registry modules)
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属性:
  • source
    - 模块源(本地路径或Registry地址)
  • version
    - 版本约束(仅适用于Registry模块)
测试本地模块:
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
state_key
attribute controls which state file a run block uses. By default:
  • The main configuration shares a state file across all run blocks
  • Each alternate module (referenced via
    module
    block) gets its own state file
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块使用的状态文件。默认情况下:
  • 主配置在所有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 = 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
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" } }
undefined
run "test_integration" { command = plan

等待上面的并行运行完成

assert { condition = output.combined != "" error_message = "集成应正常工作" } }
undefined

Mock 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 test
Run specific test file:
bash
terraform test tests/defaults.tftest.hcl
Run with verbose output:
bash
terraform test -verbose
Run tests in a specific directory:
bash
terraform test -test-directory=integration-tests
Filter tests by name:
bash
terraform test -filter=test_vpc_configuration
Run 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-cleanup

Test 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
command = plan
. These tests validate Terraform logic without creating real infrastructure, making them fast and cost-free.
以下示例展示了使用
command = plan
的常见单元测试模式。这些测试无需创建真实基础设施即可验证Terraform逻辑,速度快且无成本。

Testing 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
undefined
hcl
undefined

In 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 ] }
undefined
run "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 ] }
undefined

Integration Testing

集成测试

For tests that create real infrastructure (default behavior with
command = apply
):
hcl
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 = apply
的默认行为):
hcl
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

测试完成后自动清理

undefined
undefined

Cleanup 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-cleanup

Best Practices

最佳实践

  1. Test Organization: Organize tests by type using clear naming conventions:
    • Unit tests (plan mode):
      *_unit_test.tftest.hcl
      - fast, no resources created
    • Integration tests (apply mode):
      *_integration_test.tftest.hcl
      - creates real resources
    • Example:
      defaults_unit_test.tftest.hcl
      ,
      validation_unit_test.tftest.hcl
      ,
      full_stack_integration_test.tftest.hcl
    • This makes it easy to run unit tests separately from integration tests in CI/CD
  2. Apply vs Plan:
    • Default is
      command = apply
      (integration testing with real resources)
    • Use
      command = plan
      for unit tests (fast, no real resources)
    • Use mocks for isolated unit testing
  3. Meaningful Assertions: Write clear, specific assertion error messages that help diagnose failures
  4. Test Isolation: Each run block should be independent when possible. Use sequential runs only when testing dependencies
  5. Variable Coverage: Test different variable combinations to validate all code paths. Remember that test variables have the highest precedence
  6. Mock Providers: Use mocks for external dependencies to speed up tests and reduce costs (requires Terraform 1.7.0+)
  7. Cleanup: Integration tests automatically destroy resources in reverse order after completion. Use
    -no-cleanup
    flag for debugging
  8. CI Integration: Run
    terraform test
    in CI/CD pipelines to catch issues early
  9. Test Naming: Use descriptive names for run blocks that explain what scenario is being tested
  10. Negative Testing: Test invalid inputs and expected failures using
    expect_failures
  11. Module Support: Remember that test files only support local and registry modules, not Git or other sources
  12. Parallel Execution: Use
    parallel = true
    for independent tests with different state files to speed up test execution
  1. 测试组织:使用清晰的命名约定按类型组织测试:
    • 单元测试(plan模式):
      *_unit_test.tftest.hcl
      - 速度快,不创建资源
    • 集成测试(apply模式):
      *_integration_test.tftest.hcl
      - 创建真实资源
    • 示例:
      defaults_unit_test.tftest.hcl
      ,
      validation_unit_test.tftest.hcl
      ,
      full_stack_integration_test.tftest.hcl
    • 这使得在CI/CD中单独运行单元测试和集成测试变得容易
  2. Apply vs Plan
    • 默认是
      command = apply
      (使用真实资源的集成测试)
    • 对单元测试使用
      command = plan
      (速度快,无真实资源)
    • 对隔离的单元测试使用mocks
  3. 有意义的断言:编写清晰、具体的断言错误消息,帮助诊断失败
  4. 测试隔离:尽可能让每个run块独立。仅在测试依赖关系时使用顺序运行
  5. 变量覆盖:测试不同的变量组合以验证所有代码路径。记住测试变量优先级最高
  6. Mock Providers:对外部依赖使用mocks以加快测试速度并降低成本(需要Terraform 1.7.0+)
  7. 清理:集成测试完成后会自动按逆序销毁资源。使用
    -no-cleanup
    标志进行调试
  8. CI集成:在CI/CD流水线中运行
    terraform test
    以尽早发现问题
  9. 测试命名:为run块使用描述性名称,解释正在测试的场景
  10. 负面测试:使用
    expect_failures
    测试无效输入和预期失败
  11. 模块支持:记住测试文件仅支持本地Registry模块,不支持Git或其他源
  12. 并行执行:对具有不同状态文件的独立测试使用
    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
-verbose
flag for detailed output
问题:断言失败
解决方案:查看错误消息,检查实际值与预期值,验证变量输入。使用
-verbose
标志获取详细输出

Provider 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
    command = plan
    instead of
    apply
    where possible
  • Leverage mock providers
  • Use
    parallel = true
    for independent tests
  • Organize slow integration tests separately
问题:测试运行时间过长
解决方案
  • 尽可能使用
    command = plan
    而非
    apply
  • 利用mock providers
  • 对独立测试使用
    parallel = true
  • 将慢的集成测试单独组织

State Conflicts

状态冲突

Issue: Multiple tests interfere with each other
Solution:
  • Use different modules (automatic separate state)
  • Use
    state_key
    attribute to control state file sharing
  • 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
undefined

tests/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 ] }

```hcl
run "test_invalid_cidr" { command = plan
variables { vpc_cidr = "invalid" vpc_name = "test-vpc" }
expect_failures = [ var.vpc_cidr ] }

```hcl

tests/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" } }

```hcl
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 = "应创建具有有效ID的VPC" }
assert { condition = aws_vpc.main.state == "available" error_message = "VPC应处于可用状态" } }

```hcl

tests/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 testing
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应与输入匹配" } }
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
    - main
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
    - main

References

参考资料