Article

Testing Terraform: A Practical Guide for Engineers

by Gary Worthington, More Than Monkeys

Infrastructure as Code (IaC) is treated with the same rigour as application code. Teams version their Terraform configurations, peer-review pull requests, and run them through CI pipelines. But too often, testing is the weak link: people rely on “terraform apply” in a dev environment and hope for the best.

Terraform code is code and that means we should test it.

In this article, we’ll look at how to test Terraform effectively, from quick static checks to full end-to-end validation. Along the way we’ll use concrete examples that you can drop into your own workflows.

Why Test Terraform?

Terraform manages critical systems: networks, IAM policies, databases, production workloads. A single mistake can bring down environments or expose data. Testing provides:

  • Early feedback — catch errors before cloud resources are created.
  • Confidence in changes — know that your modules and stacks behave as intended.
  • Repeatability — automated checks ensure consistent results across environments.
  • Safer collaboration — pull requests can be merged with confidence.

Levels of Terraform Testing

Like application testing, infrastructure testing works best when layered.

1. Static Analysis

Before creating any resources, run tools that parse and lint your code.

terraform fmt -check
terraform validate
tflint
  • terraform fmt ensures a consistent style.
  • terraform validate checks syntax and internal references.
  • tflint catches best-practice violations (unused variables, deprecated arguments, provider issues).

These are fast, low-effort tests that belong in every CI pipeline.

2. Unit Tests for Modules

Modules are Terraform’s building blocks. You can unit-test them by asserting what Terraform would create, without deploying anything.

terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json

You can then write tests in Python, Go, or your language of choice that assert properties of the plan:

import json

def test_s3_bucket_has_versioning() -> None:
"""Ensure S3 buckets are created with versioning enabled."""
with open("plan.json") as f:
plan = json.load(f)
resources = [
r for r in plan["planned_values"]["root_module"]["resources"]
if r["type"] == "aws_s3_bucket"
]
for bucket in resources:
assert bucket["values"].get("versioning") == [{"enabled": True}]

This checks that all buckets in your plan are versioned. No AWS account required.

3. Integration Tests

Sometimes you want to deploy into a real environment (often a sandbox account) and test that the resources behave correctly.

Terratest is the de-facto standard: Go-based, battle-tested, and widely used.

Example (Go):

func TestTerraformNetworking(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../networking",
Vars: map[string]interface{}{
"environment": "test",
},
}

defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.True(t, strings.HasPrefix(vpcId, "vpc-"))
}

This spins up the networking stack, checks the VPC ID output, and then destroys it. You can do this against real cloud infra, or use LocalStack instead.

4. End-to-End Tests

For production-critical stacks, go further: deploy the infrastructure and run functional checks against it. For example:

  • Provision an RDS instance, then connect to it and verify queries.
  • Create an S3 bucket, then upload/download a file.
  • Deploy a Lambda and invoke it with a test event.

These tests are slower and cost money, but they prove the system really works.

Local Testing Strategies Compared

Instead of a table, here’s a quick rundown of the main approaches:

Static analysis

  • Speed: Very fast
  • Cost: Free
  • Confidence: Low
  • Best for: CI lint checks and catching obvious mistakes

Unit tests

  • Speed: Fast
  • Cost: Free
  • Confidence: Medium
  • Best for: Validating module contracts and outputs

Integration tests

  • Speed: Medium
  • Cost: Some (sandbox environments)
  • Confidence: High
  • Best for: Deploying into test accounts and checking real resources

End-to-end tests

  • Speed: Slow
  • Cost: Higher
  • Confidence: Very high
  • Best for: Production-like validation before promotion

Putting It Together

A practical setup might look like this:

  1. Pre-commit hooks: terraform fmt and tflint.
  2. CI pipeline: run terraform validate, generate a plan, and run unit tests against it.
  3. Nightly job: integration tests with Terratest in a sandbox environment.
  4. Release pipeline: selective end-to-end tests before promotion to production.

This gives fast feedback for everyday development, and deeper validation when it matters.

Worked End-to-End Example: S3 Bucket Testing

Let’s take a concrete case. Suppose Terraform provisions an S3 bucket:

resource "aws_s3_bucket" "app_bucket" {
bucket = "my-app-bucket"
}

We want to test not only that the bucket exists, but that we can upload and retrieve data.

Here’s how to do it in Python with pytest, boto3, and moto (for local mocking).

import boto3
from moto import mock_s3

BUCKET_NAME = "my-app-bucket"
@mock_s3
def test_s3_bucket_read_write() -> None:
"""Test writing and reading a file from the Terraform-provisioned bucket."""
# Arrange: create bucket (moto simulates AWS locally)
s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket=BUCKET_NAME)

# Act: upload a file
s3.put_object(Bucket=BUCKET_NAME, Key="hello.txt", Body=b"Hello, Terraform!")

# Assert: read it back
response = s3.get_object(Bucket=BUCKET_NAME, Key="hello.txt")
body = response["Body"].read()
assert body == b"Hello, Terraform!"

This test runs locally, fast, and without AWS credentials. In a CI pipeline, you could swap out moto for a real AWS account in a sandbox environment to make it a true end-to-end test.

Final Thoughts

Testing Terraform (like any form of testing) isn’t about perfection. It’s about building confidence. The right mix of static checks, plan-based assertions, integration, and end-to-end testing makes your infrastructure code as trustworthy as your application code.

If your team is only running terraform apply in dev and hoping for the best, you’ve got a gap. Close it with testing.

Gary Worthington is a software engineer, delivery consultant, and agile coach who helps teams move fast, learn faster, and scale when it matters. He writes about modern engineering, product thinking, and helping teams ship things that matter.

Through his consultancy, More Than Monkeys, Gary helps startups and scaleups improve how they build software — from tech strategy and agile delivery to product validation and team development.

Visit morethanmonkeys.co.uk to learn how we can help you build better, faster.

Follow Gary on LinkedIn for practical insights into engineering leadership, agile delivery, and team performance.