Skip to content

Testing Terraform Configurations: Validate, TFLint, TFSec, Terratest & Unit Tests

DodaTech 6 min read

In this tutorial, you'll learn about Testing Terraform Configurations: Validate, TFLint, TFSec, Terratest & Unit Tests. We cover key concepts, practical examples, and best practices.

Testing Terraform configurations combines static analysis, security scanning, unit tests, and integration tests to catch errors, enforce policies, and validate infrastructure behavior before deployment.

What You'll Learn

In this tutorial, you will learn how to implement a comprehensive Terraform testing strategy using <a href="/devops/terraform/">terraform</a> validate, <a href="/devops/terraform/">terraform</a> fmt, TFLint, TFSec, the built-in <a href="/devops/terraform/">terraform</a> test framework, and Terratest for full integration testing.

Why It Matters

Untested Terraform configurations are the leading cause of infrastructure incidents. A missing validation catches a bad CIDR block only after an apply fails. Security scanning catches public S3 buckets before data exposure. Integration tests verify that resources actually work together.

Real-World Use

DodaTech runs a five-stage Terraform testing pipeline: fmt, validate, tflint, tfsec, and terraform test. Durga Antivirus Pro's CI pipeline rejects any change that fails security scanning or unit tests, preventing misconfigured security groups and unencrypted storage from reaching production.

Testing Pipeline Overview

graph LR
    A[Code Change] --> B[terraform fmt -check]
    B --> C[terraform validate]
    C --> D[tflint]
    D --> E[tfsec]
    E --> F[terraform test]
    F --> G[Terratest]
    G --> H[terraform plan]
    H --> I[terraform apply]
    
    B -.-> J{Pass?}
    J -->|No| K[Block PR]
    J -->|Yes| C
    
    F -.-> L{Pass?}
    L -->|No| M[Block Deploy]
    L -->|Yes| G
    
    style A fill:#4a90d9,color:#fff
    style K fill:#e74c3c,color:#fff
    style H fill:#50c878,color:#fff

Static Analysis Testing

Terraform Fmt

# Check formatting in CI
terraform fmt -check -recursive

Expected output (when unformatted files exist):

main.tf
modules/vpc/main.tf

Expected output (when all formatted):

# No output, exit code 0

Terraform Validate

# Intentional error: missing required provider
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}
terraform init && terraform validate

Expected output (with error):

Error: Reference to undeclared resource
  on main.tf line 1:
  resource "aws_instance" "web" has not been declared in the root module

TFLint

# Configuration with linter warnings
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}
tflint --init && tflint

Expected output:

1 issue(s) found:

Warning: Missing version constraint for provider "aws" (terraform_required_providers)
  on main.tf line 1:
  Source: https://github.com/terraform-linters/tflint-ruleset-aws

TFSec Security Scanning

# Insecure configuration
resource "aws_s3_bucket" "data" {
  bucket = "company-data"
  acl    = "public-read"
}

resource "aws_security_group" "ssh" {
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
tfsec .

Expected output:

Results: 2 failed, 28 passed

  aws_s3_bucket.data [AWS001] - S3 bucket has public read ACL.
  aws_security_group.ssh [AWS011] - Security group rule allows ingress from 0.0.0.0/0 on port 22.

Unit Testing with Terraform Test Framework

Writing Unit Tests

# tests/instance_test.tftest.hcl
provider "aws" {
  region = "us-east-1"
}

run "validate_instance_type" {
  command = plan

  assert {
    condition     = aws_instance.web.instance_type == "t3.medium"
    error_message = "Instance type should be t3.medium"
  }
}

run "validate_security_group_count" {
  command = plan

  assert {
    condition     = length(aws_instance.web.vpc_security_group_ids) >= 2
    error_message = "Web instance must have at least 2 security groups"
  }
}

run "validate_encryption" {
  command = plan

  assert {
    condition     = aws_ebs_volume.data.encrypted
    error_message = "Data volume must be encrypted"
  }
}

run "validate_tags" {
  command = plan

  assert {
    condition     = lookup(aws_instance.web.tags, "Environment", "") == "testing"
    error_message = "Environment tag must be set to testing"
  }

  assert {
    condition     = lookup(aws_instance.web.tags, "ManagedBy", "") == "Terraform"
    error_message = "ManagedBy tag must be set to Terraform"
  }
}
terraform test

Expected output:

tests/instance_test.tftest.hcl... in progress
  run "validate_instance_type"... pass
  run "validate_security_group_count"... pass
  run "validate_encryption"... pass
  run "validate_tags"... pass
tests/instance_test.tftest.hcl... tearing down
tests/instance_test.tftest.hcl... pass

Success! 4 passed, 0 failed.

Testing with Variables

# tests/production_test.tftest.hcl
variables {
  environment    = "production"
  instance_type  = "t3.large"
  instance_count = 5
}

run "validate_production_config" {
  command = plan

  assert {
    condition     = var.instance_count >= 3
    error_message = "Production must have at least 3 instances"
  }

  assert {
    condition     = aws_db_instance.main.multi_az
    error_message = "Production database must be Multi-AZ"
  }
}

Integration Testing with Terratest

Go-Based Terratest

// test/integration_test.go
package test

import (
  "testing"
  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/gruntwork-io/terratest/modules/aws"
  "github.com/stretchr/testify/assert"
)

func TestWebServerInfrastructure(t *testing.T) {
  terraformOptions := &terraform.Options{
    TerraformDir: "../examples/web-server",
    Vars: map[string]interface{}{
      "environment":   "test",
      "instance_type": "t3.micro",
    },
  }

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

  instanceID := terraform.Output(t, terraformOptions, "instance_id")
  publicIP := terraform.Output(t, terraformOptions, "public_ip")

  // Verify instance is running
  status := aws.GetInstanceState(t, "us-east-1", instanceID)
  assert.Equal(t, "running", status)

  // Verify HTTP endpoint responds
  response := aws.HTTPGet(t, "http://"+publicIP)
  assert.Contains(t, response, "Hello from Terraform")
}
go test -v -timeout 60m ./test/

Expected output:

=== RUN   TestWebServerInfrastructure
  TestWebServerInfrastructure: Running terraform init
  TestWebServerInfrastructure: Running terraform apply
  TestWebServerInfrastructure: Verifying instance is running
  TestWebServerInfrastructure: Checking HTTP response
  TestWebServerInfrastructure: Running terraform destroy
--- PASS: TestWebServerInfrastructure (185.23s)
PASS

CI Pipeline Integration

# .github/workflows/terraform-test.yml
name: Terraform Tests
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Format Check
        run: terraform fmt -check -recursive

      - name: Init and Validate
        run: |
          terraform init
          terraform validate

      - name: Unit Tests
        run: terraform test

      - name: Security Scan
        uses: aquasecurity/tfsec-action@v1

Common Mistakes

1. Only Running Validate

<a href="/devops/terraform/">terraform</a> validate checks syntax only. It does not catch security issues, deprecated syntax, or provider-specific errors.

2. Skipping Fmt Check

Without <a href="/devops/terraform/">terraform</a> fmt -check in CI, formatting inconsistencies accumulate and make code review harder.

3. Not Scanning Secrets

TFSec does not detect hardcoded passwords in configuration. Use additional tools like trufflehog.

4. Writing Tests After Deployment

Test-driven infrastructure catches issues before they reach production. Write tests before apply, not after.

5. Ignoring Terratest for Complex Logic

Unit tests verify plan output only. Terratest provisions real resources and validates actual behavior.

Practice Questions

1. What is the difference between <a href="/devops/terraform/">terraform</a> validate and tflint? Validate checks Terraform core syntax. TFLint checks provider-specific rules, best practices, and deprecated syntax.

2. How do you write a unit test that verifies an EBS volume is encrypted? Use a <a href="/devops/terraform/">terraform</a> test file with command = plan and an assert block checking aws_ebs_volume.data.encrypted == true.

3. What does Terratest provide that <a href="/devops/terraform/">terraform</a> test does not? Terratest provisions real infrastructure and validates actual resource behavior (HTTP responses, instance states, database connections).

4. Challenge: Write a .tftest.hcl file that validates a deployment must have: at least 3 instances in production, Multi-AZ database, all EBS volumes encrypted, and no security groups open to 0.0.0.0/0 on port 22.

Mini Project: Complete Testing Pipeline

Set up a five-stage testing pipeline: terraform fmt -check, terraform validate, tflint, tfsec, and terraform test. Write unit tests for instance type, tag enforcement, and encryption requirements. Add a Terratest that provisions a web server and verifies the HTTP response.

Terraform CI/CD Pipelines
Terraform Best Practices

What's Next

Implement Terraform testing in your CI/CD pipeline, then study Best Practices for production-grade infrastructure automation. Explore DevOps testing strategies for infrastructure reliability.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro