Infrastructure Testing Patterns
Test infrastructure code with the same rigor as application code. Covers Terraform plan testing, Pulumi unit tests, integration testing with real cloud resources, compliance testing, and the patterns that prevent infrastructure failures in production.
Infrastructure code provisions servers, networks, databases, and security policies. A bug in application code shows an error message. A bug in infrastructure code opens a security hole, deletes a database, or creates an unintended $50K/month resource. Infrastructure testing is not optional — it is essential.
Testing Pyramid for Infrastructure
╱╲
╱ ╲
╱ E2E ╲ Cloud deployment tests
╱────────╲ (expensive, slow, comprehensive)
╱Integration╲ Terraform plan validation
╱──────────────╲ (moderate cost, real providers)
╱ Unit Tests ╲ Policy, schema, logic tests
╱────────────────────╲ (fast, free, many)
╱ Static Analysis ╲ Linting, formatting, scanning
╱────────────────────────╲ (instant, free)
Static Analysis
# Terraform
terraform fmt -check # Formatting
terraform validate # Syntax + schema
tflint # Best practices
tfsec # Security scanning
checkov --directory . # Compliance policies
# Pulumi
pulumi preview # Dry-run
eslint ./infra/**/*.ts # Code linting
Unit Testing (Policy as Code)
OPA/Conftest
# policy/terraform.rego
package terraform
deny[msg] {
resource := input.planned_values.root_module.resources[_]
resource.type == "aws_s3_bucket"
not resource.values.server_side_encryption_configuration
msg := sprintf("S3 bucket '%s' must have encryption", [resource.name])
}
deny[msg] {
resource := input.planned_values.root_module.resources[_]
manageable_resources := {"aws_instance", "aws_s3_bucket", "aws_rds_db_instance"}
manageable_resources[resource.type]
not resource.values.tags.Team
msg := sprintf("Resource '%s' must have a Team tag", [resource.address])
}
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json --policy policy/
Pulumi Unit Tests
import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect } from "vitest";
pulumi.runtime.setMocks({
newResource: (args) => ({ id: `${args.name}-id`, state: args.inputs }),
call: (args) => args.inputs,
});
describe("S3 Bucket", () => {
it("should have encryption enabled", async () => {
const { bucket } = await import("./s3");
const encryption = await new Promise<any>((resolve) => {
bucket.serverSideEncryptionConfiguration.apply(resolve);
});
expect(encryption).toBeDefined();
expect(encryption.rule.applyServerSideEncryptionByDefault.sseAlgorithm)
.toBe("aws:kms");
});
it("should block public access", async () => {
const { publicAccessBlock } = await import("./s3");
const blockPublicAcls = await new Promise<boolean>((resolve) => {
publicAccessBlock.blockPublicAcls.apply(resolve);
});
expect(blockPublicAcls).toBe(true);
});
});
Integration Testing with Terratest
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/stretchr/testify/assert"
)
func TestS3BucketCreation(t *testing.T) {
t.Parallel()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/s3",
Vars: map[string]interface{}{
"bucket_name": "test-bucket-" + random.UniqueId(),
"environment": "test",
},
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
bucketName := terraform.Output(t, terraformOptions, "bucket_name")
region := terraform.Output(t, terraformOptions, "bucket_region")
encryption := aws.GetS3BucketEncryption(t, region, bucketName)
assert.Equal(t, "aws:kms", encryption)
versioning := aws.GetS3BucketVersioning(t, region, bucketName)
assert.Equal(t, "Enabled", versioning)
}
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| No infrastructure testing | Bugs found in production | Testing pyramid from static to integration |
Testing only with terraform plan | Plan looks fine but apply fails | Integration tests with real resources |
| No policy enforcement | Security misconfigurations deployed | OPA/Conftest policies in CI |
| Manual infrastructure reviews | Subjective, inconsistent | Automated policy checks + human review |
| Integration tests without cleanup | Orphaned test resources, cost waste | defer destroy, test resource tagging |
Infrastructure testing is the safety net between your Terraform code and your production environment. Every untested change is a potential outage, security hole, or surprise bill.