Pulumi Testing & Validation

Medium 25 min read

Testing Overview

Why Test Infrastructure Code?

The Problem: Infrastructure bugs are expensive. A misconfigured security group or wrong IAM policy can cause outages or security breaches that take hours to diagnose.

The Solution: Pulumi lets you write tests in the same language as your infrastructure code, catching errors before they reach production.

Real Impact: Organizations with tested IaC report 90% fewer infrastructure-related incidents and 5x faster rollback times.

Real-World Analogy

Think of testing Pulumi code like testing a building before people move in:

  • Unit Tests = Inspecting individual components (wiring, plumbing) before installation
  • Property Tests = Checking that every room meets building codes (no public S3 buckets)
  • Integration Tests = Walking through the entire building to verify everything works together
  • Mocks = Using architectural models instead of building real structures for each test

The Testing Pyramid for IaC

Unit Tests

Test individual resource configurations using mocks. Fast, cheap, and catch configuration errors early.

Property Tests

Validate resource properties across your entire stack. Ensure compliance policies are met before deployment.

Integration Tests

Deploy real infrastructure in a test environment using the Automation API and verify it works end-to-end.

Policy Tests

Use CrossGuard policy packs to enforce organizational standards across all Pulumi programs.

Testing Pyramid for Infrastructure as Code
Unit Tests Fast | Mocked | Many Property Tests Validate | Compliance | Medium Integration Real | Slow | Few Milliseconds Seconds Minutes Cost & Confidence

Unit Testing

Setting Up Mocks

index.test.ts
import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect, beforeAll } from "vitest";

// Mock Pulumi runtime before importing resources
pulumi.runtime.setMocks({
    newResource: (args: pulumi.runtime.MockResourceArgs) => {
        return {
            id: `${args.name}-id`,
            state: {
                ...args.inputs,
                arn: `arn:aws:s3:::${args.inputs.bucket || args.name}`,
            },
        };
    },
    call: (args: pulumi.runtime.MockCallArgs) => {
        return args.inputs;
    },
});

describe("S3 Bucket", () => {
    let infra: typeof import("./index");

    beforeAll(async () => {
        infra = await import("./index");
    });

    it("should have versioning enabled", async () => {
        const versioning = await new Promise(resolve =>
            infra.bucketVersioning.apply(v => resolve(v))
        );
        expect(versioning).toBe("Enabled");
    });

    it("should block public access", async () => {
        const blockPublic = await new Promise(resolve =>
            infra.publicAccessBlocked.apply(v => resolve(v))
        );
        expect(blockPublic).toBe(true);
    });
});

Property Testing

Validating Resource Properties

property-tests.ts
import * as policy from "@pulumi/policy";

const stackPolicy = new policy.PolicyPack("tests", {
    policies: [
        {
            name: "s3-no-public-read",
            description: "S3 buckets must not allow public read",
            enforcementLevel: "mandatory",
            validateResource: policy.validateResourceOfType(
                aws.s3.BucketV2,
                (bucket, args, reportViolation) => {
                    // Check property tests on each S3 bucket
                }
            ),
        },
        {
            name: "ec2-must-have-tags",
            description: "All EC2 instances must have required tags",
            enforcementLevel: "mandatory",
            validateResource: policy.validateResourceOfType(
                aws.ec2.Instance,
                (instance, args, reportViolation) => {
                    if (!instance.tags || !instance.tags["Environment"]) {
                        reportViolation("EC2 instances must have an 'Environment' tag");
                    }
                    if (!instance.tags || !instance.tags["Team"]) {
                        reportViolation("EC2 instances must have a 'Team' tag");
                    }
                }
            ),
        },
    ],
});

Integration Testing

Using the Automation API

integration.test.ts
import { LocalWorkspace } from "@pulumi/pulumi/automation";
import { describe, it, expect, afterAll } from "vitest";

describe("Infrastructure Integration", () => {
    let stack: any;

    it("should deploy successfully", async () => {
        stack = await LocalWorkspace.createOrSelectStack({
            stackName: "test",
            workDir: "./infra",
        });

        await stack.setConfig("aws:region", { value: "us-east-1" });

        const upResult = await stack.up({ onOutput: console.log });
        expect(upResult.summary.result).toBe("succeeded");
    }, 300000);

    it("should have correct outputs", async () => {
        const outputs = await stack.outputs();
        expect(outputs.bucketName.value).toBeDefined();
        expect(outputs.vpcId.value).toMatch(/^vpc-/);
    });

    afterAll(async () => {
        if (stack) {
            await stack.destroy({ onOutput: console.log });
            await stack.workspace.removeStack("test");
        }
    });
});

Mocking Resources

advanced-mocks.ts
// Advanced mock that simulates realistic resource behavior
pulumi.runtime.setMocks({
    newResource: (args) => {
        switch (args.type) {
            case "aws:ec2/instance:Instance":
                return {
                    id: "i-1234567890abcdef0",
                    state: {
                        ...args.inputs,
                        publicIp: "54.123.45.67",
                        privateIp: "10.0.1.100",
                        arn: `arn:aws:ec2:us-east-1:123456789:instance/${args.name}`,
                    },
                };
            case "aws:s3/bucketV2:BucketV2":
                return {
                    id: args.name,
                    state: {
                        ...args.inputs,
                        bucket: `${args.name}-abc123`,
                        arn: `arn:aws:s3:::${args.name}`,
                    },
                };
            default:
                return { id: `${args.name}-id`, state: args.inputs };
        }
    },
    call: (args) => args.inputs,
});

Quick Reference

Test TypeSpeedReal Resources?Best For
Unit TestsMillisecondsNo (mocked)Configuration validation, logic checks
Property TestsSecondsNo (policy)Compliance, tagging, security rules
Integration TestsMinutesYes (deployed)End-to-end verification, smoke tests
Policy (CrossGuard)SecondsNo (pre-deploy)Organization-wide enforcement

Testing Best Practices

  • Run unit tests on every commit - they are fast and catch most issues
  • Use property tests to enforce company-wide compliance policies
  • Run integration tests in CI/CD against an isolated test account
  • Always clean up test stacks to avoid resource leaks and costs
  • Use pulumi.runtime.setMocks() to avoid real API calls in unit tests