Terraform Testing and Validation Techniques

Infrastructure code that is never tested accumulates hidden assumptions and silent failures. A misconfigured security group, a broken module output, or a wrong CIDR calculation might not reveal itself until it causes a production incident. Terraform provides several tools for catching problems early — from fast static checks to full end-to-end infrastructure tests. This topic covers each level of validation from quickest to most thorough.

The Testing Pyramid for Infrastructure

Diagram: Levels of Terraform Testing

                    /\
                   /  \
                  / E2E \       ← terraform test (full apply in real cloud)
                 /--------\
                /Integration\   ← module tests with real resources
               /--------------\
              /  Unit / Static  \  ← fmt, validate, plan, checkov
             /--------------------\

Faster, cheaper, safer at the bottom.
More realistic, more expensive at the top.

Start with the bottom layers — they catch the majority of errors in seconds. Add higher layers for critical modules and shared infrastructure code.

Level 1: terraform fmt

The fastest check. Terraform's formatter ensures your code follows the canonical style. Run it as a lint check in CI — fail the pipeline if the code is not properly formatted.

# Check format without modifying files (returns exit code 1 if unformatted)
terraform fmt -check -recursive

# Fix formatting automatically
terraform fmt -recursive

Teams that run terraform fmt in CI never argue about indentation, alignment, or spacing in code reviews.

Level 2: terraform validate

Validates that your configuration is syntactically correct and internally consistent — no missing required arguments, no unknown attributes, no type mismatches. It does not contact any cloud provider.

terraform validate

Runs in seconds. Catches typos, wrong argument names, and type errors before any cloud API call is made.

Level 3: Variable Validation Blocks

Write validation rules directly in variable declarations to enforce business rules at plan time.

variable "environment" {
  type = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Valid environments are: dev, staging, prod."
  }
}

variable "instance_count" {
  type = number

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "Instance count must be between 1 and 10."
  }
}

These run on every terraform plan. A wrong environment name or out-of-range count fails with your custom error message before any resource is evaluated.

Level 4: Static Analysis with Checkov

Checkov is an open-source static analysis tool that scans Terraform code for security and compliance misconfigurations. It checks hundreds of rules — S3 buckets without encryption, security groups open to the internet, missing access logs, and more.

# Install checkov
pip install checkov

# Scan current directory
checkov -d .

# Scan and output only failed checks
checkov -d . --quiet

Example Checkov Output

Check: CKV_AWS_20: "Ensure the S3 bucket has access control list (ACL) applied"
  FAILED for resource: aws_s3_bucket.app_data
  File: /main.tf:15-25

Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
  FAILED for resource: aws_s3_bucket.app_data
  File: /main.tf:15-25

Add Checkov to CI pipelines as a gate — fail the PR if critical security checks do not pass.

Level 5: terraform test (Native Testing Framework)

Terraform 1.6 introduced a built-in testing framework. Write tests in .tftest.hcl files. Each test runs a real terraform apply in a temporary workspace, asserts conditions on the outputs or resource attributes, and then destroys everything.

Test File Structure

# tests/basic_vpc.tftest.hcl

run "creates_vpc_with_correct_cidr" {
  command = apply

  variables {
    vpc_cidr    = "10.0.0.0/16"
    environment = "test"
  }

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC ID should not be empty after apply"
  }

  assert {
    condition     = output.vpc_cidr == "10.0.0.0/16"
    error_message = "VPC CIDR does not match the requested value"
  }
}

Running Tests

terraform test

Terraform finds all .tftest.hcl files, runs each test case, reports passes and failures, and destroys all test resources. Tests that take too long or cost too much to run against real cloud can use command = plan instead of command = apply — running only to the plan stage without creating real infrastructure.

Level 6: Terratest (Go-based Integration Testing)

Terratest is an open-source Go library from Gruntwork for writing full integration tests for Terraform modules. Tests deploy real infrastructure, make HTTP requests, run SSH commands, and verify behaviour — then tear everything down.

// Example Terratest structure (Go)
func TestVpcModule(t *testing.T) {
    opts := &terraform.Options{
        TerraformDir: "../modules/networking",
        Vars: map[string]interface{}{
            "vpc_cidr":    "10.0.0.0/16",
            "environment": "test",
        },
    }

    defer terraform.Destroy(t, opts)
    terraform.InitAndApply(t, opts)

    vpcID := terraform.Output(t, opts, "vpc_id")
    assert.NotEmpty(t, vpcID)
}

Terratest is best suited for shared, widely-used module libraries where confidence in correctness justifies the extra complexity and cost of full cloud tests.

Key Points

  • Use terraform fmt -check and terraform validate as fast first-pass checks in every CI pipeline.
  • Add variable validation blocks to enforce business rules at plan time with readable error messages.
  • Run Checkov or a similar static analysis tool to catch security misconfigurations before deployment.
  • Use the native terraform test framework (Terraform 1.6+) for writing structured, repeatable module tests.
  • Apply testing effort proportionally — shared modules used across many teams deserve more thorough testing than single-use configurations.

Leave a Comment