terraform-test

โดย hashicorp

คู่มือที่ครอบคลุมสำหรับการเขียนและรันการทดสอบ Terraform พร้อมการยืนยันผล การจำลอง และการตรวจสอบโมดูล เขียนไฟล์ทดสอบโดยใช้ไวยากรณ์ .tftest.hcl พร้อมบล็อก run ที่ทำงานในโหมด plan หรือ apply รองรับการทำงานแบบลำดับและแบบขนานพร้อมการแยกสถานะที่เป็นทางเลือก ยืนยันเงื่อนไขบนแอตทริบิวต์ของทรัพยากร เอาต์พุต และแหล่งข้อมูล ใช้ expect_failures เพื่อตรวจสอบว่าอินพุตที่ไม่ถูกต้องถูกปฏิเสธอย่างเหมาะสม จำลองผู้ให้บริการ (Terraform 1.7.0+) เพื่อจำลองพฤติกรรมของโครงสร้างพื้นฐานโดยไม่ต้อง...

npx skills add https://github.com/hashicorp/agent-skills --skill terraform-test

Terraform Test

Terraform's built-in testing framework validates that configuration updates don't introduce breaking changes. Tests run against temporary resources, protecting existing infrastructure and state files.

Reference Files

  • references/MOCK_PROVIDERS.md — Mock provider syntax, common defaults, when to use mocks (Terraform 1.7.0+ only — skip if the user's version is below 1.7)
  • references/CI_CD.md — GitHub Actions and GitLab CI pipeline examples
  • references/EXAMPLES.md — Complete example test suite (unit, integration, and mock tests for a VPC module)

Read the relevant reference file when the user asks about mocking, CI/CD integration, or wants a full example.

Core Concepts

  • Test file (.tftest.hcl / .tftest.json): Contains run blocks that validate your configuration
  • Run block: A single test scenario with optional variables, providers, and assertions
  • Assert block: Conditions that must be true for the test to pass
  • Mock provider: Simulates provider behavior without real infrastructure (Terraform 1.7.0+)
  • Test modes: apply (default, creates real resources) or plan (validates logic only)

File Structure

my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
    ├── defaults_unit_test.tftest.hcl         # plan mode — fast, no resources
    ├── validation_unit_test.tftest.hcl        # plan mode
    └── full_stack_integration_test.tftest.hcl # apply mode — creates real resources

Use *_unit_test.tftest.hcl for plan-mode tests and *_integration_test.tftest.hcl for apply-mode tests so they can be filtered separately in CI.

Test File Structure

# Optional: test-wide settings
test {
  parallel = true  # Enable parallel execution for all run blocks (default: false)
}

# Optional: file-level variables (highest precedence, override all other sources)
variables {
  aws_region    = "us-west-2"
  instance_type = "t2.micro"
}

# Optional: provider configuration
provider "aws" {
  region = var.aws_region
}

# Required: at least one run block
run "test_default_configuration" {
  command = plan

  assert {
    condition     = aws_instance.example.instance_type == "t2.micro"
    error_message = "Instance type should be t2.micro by default"
  }
}

Run Block

run "test_name" {
  command  = plan  # or apply (default)
  parallel = true  # optional, since v1.9.0

  # Override file-level variables
  variables {
    instance_type = "t3.large"
  }

  # Reference a specific module
  module {
    source  = "./modules/vpc"  # local or registry only (not git/http)
    version = "5.0.0"          # registry modules only
  }

  # Control state isolation
  state_key = "shared_state"  # since v1.9.0

  # Plan behavior
  plan_options {
    mode    = refresh-only  # or normal (default)
    refresh = true
    replace = [aws_instance.example]
    target  = [aws_instance.example]
  }

  # Assertions
  assert {
    condition     = aws_instance.example.id != ""
    error_message = "Instance should have a valid ID"
  }

  # Expected failures (test passes if these fail)
  expect_failures = [
    var.instance_count
  ]
}

Common Test Patterns

Validate outputs

run "test_outputs" {
  command = plan

  assert {
    condition     = output.vpc_id != null
    error_message = "VPC ID output must be defined"
  }

  assert {
    condition     = can(regex("^vpc-", output.vpc_id))
    error_message = "VPC ID should start with 'vpc-'"
  }
}

Conditional resources

run "test_nat_gateway_disabled" {
  command = plan

  variables {
    create_nat_gateway = false
  }

  assert {
    condition     = length(aws_nat_gateway.main) == 0
    error_message = "NAT gateway should not be created when disabled"
  }
}

Resource counts

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

Tags

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

Data sources

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

Validation rules

run "test_invalid_environment" {
  command = plan

  variables {
    environment = "invalid"
  }

  expect_failures = [
    var.environment
  ]
}

Sequential tests with dependencies

run "setup_vpc" {
  command = apply

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC should be created"
  }
}

run "test_subnet_in_vpc" {
  command = plan

  variables {
    vpc_id = run.setup_vpc.vpc_id
  }

  assert {
    condition     = aws_subnet.example.vpc_id == run.setup_vpc.vpc_id
    error_message = "Subnet should be in the VPC from setup_vpc"
  }
}

Plan options (refresh-only, targeted)

run "test_refresh_only" {
  command = plan

  plan_options {
    mode = refresh-only
  }

  assert {
    condition     = aws_instance.example.tags["Environment"] == "production"
    error_message = "Tags should be refreshed correctly"
  }
}

run "test_specific_resource" {
  command = plan

  plan_options {
    target = [aws_instance.example]
  }

  assert {
    condition     = aws_instance.example.instance_type == "t2.micro"
    error_message = "Targeted resource should be planned"
  }
}

Parallel modules

run "test_networking_module" {
  command  = plan
  parallel = true

  module {
    source = "./modules/networking"
  }

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC should be created"
  }
}

run "test_compute_module" {
  command  = plan
  parallel = true

  module {
    source = "./modules/compute"
  }

  assert {
    condition     = output.instance_id != ""
    error_message = "Instance should be created"
  }
}

State key sharing

run "create_foundation" {
  command   = apply
  state_key = "foundation"

  assert {
    condition     = aws_vpc.main.id != ""
    error_message = "Foundation VPC should be created"
  }
}

run "create_application" {
  command   = apply
  state_key = "foundation"

  variables {
    vpc_id = run.create_foundation.vpc_id
  }

  assert {
    condition     = aws_instance.app.vpc_id == run.create_foundation.vpc_id
    error_message = "Application should use foundation VPC"
  }
}

Cleanup ordering (S3 objects before bucket)

run "create_bucket" {
  command = apply

  assert {
    condition     = aws_s3_bucket.example.id != ""
    error_message = "Bucket should be created"
  }
}

run "add_objects" {
  command = apply

  assert {
    condition     = length(aws_s3_object.files) > 0
    error_message = "Objects should be added"
  }
}

# Cleanup destroys in reverse: objects first, then bucket

Multiple aliased providers

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

Complex conditions

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

Cleanup

Resources are destroyed in reverse run block order after test completion. This matters for dependencies (e.g., S3 objects before bucket). Use terraform test -no-cleanup to skip cleanup for debugging.

Running Tests

terraform test                                        # all tests
terraform test tests/defaults.tftest.hcl             # specific file
terraform test -filter=test_vpc_configuration        # by run block name
terraform test -test-directory=integration-tests     # custom directory
terraform test -verbose                              # detailed output
terraform test -no-cleanup                           # skip resource cleanup

Best Practices

  1. Naming: *_unit_test.tftest.hcl for plan mode, *_integration_test.tftest.hcl for apply mode
  2. Test naming: Use descriptive run block names that explain the scenario being tested
  3. Default to plan: Use command = plan unless you need to test real resource behavior
  4. Use mocks for external dependencies — faster and no credentials needed (see references/MOCK_PROVIDERS.md)
  5. Error messages: Make them specific enough to diagnose failures without running the test again
  6. Negative tests: Use expect_failures to verify validation rules reject bad inputs
  7. Variable coverage: Test different variable combinations to validate all code paths — test variables have the highest precedence and override all other sources
  8. Module sources: Test files only support local paths and registry modules — not git or HTTP URLs
  9. Parallel execution: Use parallel = true for independent tests with different state files
  10. Cleanup: Integration tests destroy resources in reverse run block order automatically; use -no-cleanup for debugging
  11. CI/CD: Run unit tests on every PR, integration tests on merge (see references/CI_CD.md)

Troubleshooting

IssueSolution
Assertion failuresUse -verbose to see actual vs expected values
Missing credentialsUse mock providers for unit tests
Unsupported module sourceConvert git/HTTP sources to local modules
Tests interferingUse state_key or separate modules for isolation
Slow testsUse command = plan and mocks; run integration tests separately

References

Skills เพิ่มเติมจาก hashicorp

provider-actions
hashicorp
Implement การทำงานของ Terraform Provider โดยใช้ Plugin Framework ใช้เมื่อพัฒนาการดำเนินการแบบ imperative ที่ทำงานในเหตุการณ์ lifecycle (ก่อน/หลัง…
official
new-terraform-provider
hashicorp
ใช้สิ่งนี้เมื่อสร้าง Terraform provider ใหม่ด้วย Plugin Framework: โครงสร้าง workspace, การตั้งค่า go module, provider server main.go และ provider.go…
official
terraform-test
hashicorp
คู่มือที่ครอบคลุมสำหรับการเขียนและรันการทดสอบ Terraform ใช้เมื่อสร้างไฟล์ทดสอบ (.tftest.hcl) เขียนสถานการณ์ทดสอบด้วย run blocks ตรวจสอบความถูกต้อง…
official
provider-actions
hashicorp
ใช้การดำเนินการของ Terraform Provider แบบ Imperative ในเหตุการณ์วงจรชีวิตของทรัพยากรโดยใช้ Plugin Framework รองรับทริกเกอร์วงจรชีวิตก่อน/หลังการสร้างและก่อน/หลังการอัปเดต (เหตุการณ์การทำลายไม่พร้อมใช้งานใน Terraform 1.14.0) ต้องมีการกำหนดสคีมาที่ถูกต้องพร้อมประเภทเฟรมเวิร์กที่เหมาะสม ElementType สำหรับคอลเล็กชัน และตัวตรวจสอบสำหรับการตรวจสอบอินพุต รวมถึงการรายงานความคืบหน้า การจัดการหมดเวลา และการจัดการข้อผิดพลาดที่ครอบคลุมสำหรับการดำเนินการที่ใช้เวลานาน รองรับการโพลและ...
official
aws-ami-builder
hashicorp
สร้าง Amazon Machine Images แบบกำหนดเองด้วย builder amazon-ebs ของ Packer อัตโนมัติการสร้าง AMI จาก AMI ต้นทางโดยใช้เทมเพลต HCL พร้อม provisioners สำหรับการปรับแต่ง (สคริปต์เชลล์, อัปโหลดไฟล์, การจัดการการกำหนดค่า) รองรับการกระจาย AMI หลายภูมิภาคผ่าน ami_regions และการกรอง AMI ต้นทางแบบยืดหยุ่นตามชื่อ, เจ้าของ, และประเภทการจำลองเสมือน ยืนยันตัวตนผ่านตัวแปรสภาพแวดล้อม, ไฟล์ข้อมูลรับรอง AWS, หรือโปรไฟล์อินสแตนซ์ IAM; รวมคำสั่งตรวจสอบและสร้างสำหรับเทมเพลต...
official
new-terraform-provider
hashicorp
สร้างโครงร่างผู้ให้บริการ Terraform ใหม่โดยใช้ Plugin Framework สร้างพื้นที่ทำงานโมดูล Go ใหม่ตามรูปแบบการตั้งชื่อมาตรฐาน "terraform-provider-" และเริ่มต้นการพึ่งพาที่จำเป็น ให้ไฟล์ main.go ต้นแบบตามรูปแบบ Plugin Framework ของ HashiCorp พร้อมเครื่องหมาย TODO สำหรับการปรับแต่ง ตรวจสอบการตั้งค่าโดยรันคำสั่ง build และ test เพื่อให้แน่ใจว่าผู้ให้บริการคอมไพล์และผ่านการตรวจสอบเบื้องต้น จัดการพื้นที่ทำงานโดยยืนยันความตั้งใจก่อนสร้างใหม่...
official
azure-verified-modules
hashicorp
ข้อกำหนดการรับรองและแนวปฏิบัติที่ดีที่สุดสำหรับโมดูล Azure Terraform ที่ต้องการความสอดคล้องกับ AVM บังคับใช้ข้อจำกัดเวอร์ชันของ provider (azurerm >= 4.0, < 5.0; azapi >= 2.0, < 3.0) และห้ามการอ้างอิงโมดูลแบบ git โดยกำหนดให้ใช้แหล่งที่มาจาก Terraform registry ที่ถูกตรึงไว้ กำหนดให้ใช้ snake_casing ตัวพิมพ์เล็กสำหรับ identifiers ทั้งหมด ชนิดตัวแปรที่แม่นยำ แอตทริบิวต์เอาต์พุตแบบแยกส่วนผ่านรูปแบบ anti-corruption layer และ locals ที่เรียงตามลำดับตัวอักษร กำหนดให้มีตัวแปรสลับฟีเจอร์สำหรับทรัพยากรใหม่ที่ถูกเพิ่ม...
official
provider-docs
hashicorp
สร้าง อัปเดต และตรวจสอบเอกสารประกอบของ Terraform provider สำหรับ Terraform Registry โดยใช้รูปแบบที่ HashiCorp แนะนำ เทมเพลต tfplugindocs และ schema…
official