Unit Testing
Unit testing ใน Terraform = test logic ของ config โดยไม่ต้อง apply จริง — ใช้
terraform testframework (1.6+)
Terraform Test Framework
ตั้งแต่ Terraform 1.6+ มี built-in test framework:
tests/main.tftest.hcl
run "create_bucket" {
command = plan
variables {
bucket_name = "test-bucket"
}
assert {
condition = aws_s3_bucket.main.bucket == "test-bucket"
error_message = "Bucket name doesn't match input"
}
}
รัน:
terraform test
Test File Structure
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── plan_only.tftest.hcl # plan-based tests
└── integration.tftest.hcl # apply-based tests
Test Run Block
run "<NAME>" {
command = plan # หรือ apply
module = ... # optional
variables { ... }
providers { ... }
assert { ... }
}
Plan Test (Fast, no real resources)
tests/plan.tftest.hcl
variables {
region = "us-east-1"
}
run "validate_bucket_name" {
command = plan
variables {
bucket_name = "valid-bucket-name"
}
assert {
condition = aws_s3_bucket.main.bucket == "valid-bucket-name"
error_message = "Bucket name must match input"
}
}
run "fail_on_invalid_bucket" {
command = plan
variables {
bucket_name = "INVALID NAME" # contains space
}
expect_failures = [var.bucket_name] # expect validation to fail
}
Apply Test (Real resources)
tests/apply.tftest.hcl
run "deploy_test" {
command = apply
variables {
environment = "test"
}
assert {
condition = aws_s3_bucket.main.region == "us-east-1"
error_message = "Bucket region must be us-east-1"
}
}
Apply Tests = Real Cost
- ✅ ทดสอบ behavior จริง
- ❌ สร้าง/ลบ resource จริง — เสียเงิน
- ⚠️ Cleanup ผ่าน destroy หลัง test เสร็จ
Test Multiple Scenarios
variables {
region = "us-east-1"
}
run "single_instance" {
command = plan
variables {
instance_count = 1
}
assert {
condition = length(aws_instance.web) == 1
error_message = "Expected 1 instance"
}
}
run "three_instances" {
command = plan
variables {
instance_count = 3
}
assert {
condition = length(aws_instance.web) == 3
error_message = "Expected 3 instances"
}
}
run "tag_validation" {
command = plan
variables {
instance_count = 1
tags = {
Environment = "test"
}
}
assert {
condition = aws_instance.web[0].tags["Environment"] == "test"
error_message = "Tag not propagated"
}
}
Mock Provider
mock_provider "aws" {
override_resource {
target = aws_s3_bucket.main
values = {
arn = "arn:aws:s3:::mocked-bucket"
id = "mocked-bucket"
}
}
}
run "test_with_mock" {
command = plan
assert {
condition = output.bucket_arn == "arn:aws:s3:::mocked-bucket"
error_message = "Mock not applied"
}
}
→ Test โดยไม่เรียก AWS จริง — เร็วกว่า + ฟรี
Variables Block
ตั้งค่า default ของ variables สำหรับทุก run:
variables {
region = "us-east-1"
tags = {
Environment = "test"
}
}
run "test_1" {
variables {
bucket_name = "bucket-1" # override บางตัว
}
# ...
}
run "test_2" {
variables {
bucket_name = "bucket-2"
}
}
Provider Block
provider "aws" {
region = "us-east-1"
}
run "test" {
command = plan
# ใช้ provider ที่ define ข้างบน
}
Test Other Module
run "test_module" {
command = plan
module {
source = "./modules/different-module"
}
variables {
foo = "bar"
}
assert {
condition = ...
error_message = ...
}
}
ตัวอย่าง: Module Test
modules/web-app/main.tf
resource "aws_alb" "this" {
name = "${var.name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.subnet_ids
}
resource "aws_security_group" "alb" {
name = "${var.name}-alb-sg"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
modules/web-app/tests/main.tftest.hcl
variables {
name = "test"
vpc_id = "vpc-12345"
subnet_ids = ["subnet-aaa", "subnet-bbb"]
}
mock_provider "aws" {}
run "alb_name_format" {
command = plan
assert {
condition = aws_alb.this.name == "test-alb"
error_message = "ALB name should be test-alb"
}
}
run "alb_security_group" {
command = plan
assert {
condition = length(aws_security_group.alb.ingress) == 1
error_message = "ALB SG should have 1 ingress rule"
}
assert {
condition = aws_security_group.alb.ingress[0].from_port == 80
error_message = "ALB SG ingress should be port 80"
}
}
run "subnet_count" {
command = plan
variables {
subnet_ids = ["subnet-1"] # only 1 subnet
}
expect_failures = [var.subnet_ids]
}
Run Tests
# Run all tests
terraform test
# Verbose
terraform test -verbose
# Specific test file
terraform test -filter=tests/plan.tftest.hcl
# JSON output
terraform test -json
Output:
tests/plan.tftest.hcl... in progress
run "alb_name_format"... pass
run "alb_security_group"... pass
run "subnet_count"... pass
tests/plan.tftest.hcl... pass
Success! 3 passed, 0 failed.
Test Outputs
run "test_outputs" {
command = apply
assert {
condition = output.bucket_name == var.bucket_name
error_message = "Output bucket_name should match input"
}
assert {
condition = startswith(output.bucket_arn, "arn:aws:s3:::")
error_message = "Bucket ARN format invalid"
}
}
CI/CD Integration
.github/workflows/test.yml
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Run Tests
run: terraform test
Best Practices
✅ DO:
- เขียน tests สำหรับทุก module
- ใช้ mock_provider สำหรับ unit tests (เร็ว + ฟรี)
- ใช้ apply tests เฉพาะ critical paths
- Cleanup หลัง apply tests (Terraform auto destroy)
- Test edge cases (count=0, empty list, invalid input)
❌ DON'T:
- ห้าม run apply tests ใน prod account
- ห้าม skip cleanup
- ห้าม test logic ของ provider เอง (provider's job)
Limitations
- Test framework เพิ่งมาใน Terraform 1.6+ — ยังไม่มี mocking เต็มที่เท่า Terratest
- ไม่รองรับ assertion library ที่ rich (ใช้ HCL expression)
- Apply tests ช้า + ใช้เงิน — ใช้ plan + mock เป็นหลัก
→ สำหรับ integration test ที่ซับซ้อน ใช้ Terratest แทน
ตัวอย่าง: Test Suite ครบ
tests/all.tftest.hcl
variables {
region = "us-east-1"
environment = "test"
}
mock_provider "aws" {}
# Validation tests
run "valid_environment" {
command = plan
variables {
environment = "prod"
}
}
run "invalid_environment" {
command = plan
variables {
environment = "BAD"
}
expect_failures = [var.environment]
}
# Behavior tests
run "default_instance_type" {
command = plan
assert {
condition = aws_instance.web.instance_type == "t3.micro"
error_message = "Default instance type should be t3.micro"
}
}
run "production_instance_type" {
command = plan
variables {
environment = "prod"
}
assert {
condition = aws_instance.web.instance_type == "t3.large"
error_message = "Production should use t3.large"
}
}
# Tag tests
run "common_tags_applied" {
command = plan
assert {
condition = contains(keys(aws_instance.web.tags), "Environment")
error_message = "Environment tag missing"
}
assert {
condition = aws_instance.web.tags["Environment"] == "test"
error_message = "Environment tag wrong value"
}
}
# Output tests
run "outputs_correct" {
command = plan
assert {
condition = output.instance_type == aws_instance.web.instance_type
error_message = "Output instance_type doesn't match resource"
}
}
สรุป
terraform test(1.6+) = built-in test framework- Use plan tests (fast) + apply tests (real)
- mock_provider สำหรับ unit tests
- Test files:
tests/*.tftest.hcl - ใช้ assert เพื่อตรวจ condition
- รัน
terraform testใน CI ป้องกัน regression
ต่อไป → Contract Testing