TL;DR

  • Pulumi’s killer feature: use your language’s native test frameworks (Jest, pytest, Go testing) instead of learning HCL-specific tools
  • Unit tests with pulumi.runtime.setMocks() run 60x faster than integration tests—one team cut suite time from 20 minutes to 20 seconds
  • Property tests (CrossGuard) catch policy violations during deployment, not after—shift-left for infrastructure

Best for: Teams using TypeScript, Python, or Go who want CI/CD-integrated infrastructure testing Skip if: You have simple infrastructure (<10 resources) or prefer Terraform’s ecosystem Read time: 11 minutes

Testing infrastructure code in 2026 isn’t optional—it’s table stakes. But here’s what most guides won’t tell you: Pulumi’s testing approach is fundamentally different from Terraform’s, and that difference is why companies are switching.

While Terraform teams reach for external tools like Terratest or rely on terraform validate, Pulumi lets you write tests in the same language as your infrastructure. Your existing pytest knowledge? It works. Your Jest setup? It works. Your team’s CI/CD pipeline? No special plugins needed.

The Testing Pyramid for Infrastructure

Before diving into code, understand where each test type fits:

Test TypeSpeedWhat It ValidatesWhen It Runs
Unit Tests~20 secondsLogic, configuration, resource propertiesEvery commit
Property TestsDuring deploymentPolicy compliance, security rulespulumi up
Integration Tests5-30 minutesReal cloud behavior, end-to-end flowsPre-merge, nightly

Most teams get this wrong: they skip unit tests (“infrastructure is different”) and rely solely on integration tests. The result? Slow feedback loops and flaky CI pipelines.

Unit Testing with Mocks

Pulumi’s mocking system intercepts all cloud provider calls, letting you test resource configuration without provisioning anything.

TypeScript with Jest

import * as pulumi from "@pulumi/pulumi";
import "jest";

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

    beforeAll(() => {
        pulumi.runtime.setMocks({
            newResource: (args: pulumi.runtime.MockResourceArgs) => ({
                id: `${args.name}-id`,
                state: {
                    ...args.inputs,
                    arn: `arn:aws:s3:::${args.inputs.bucket || args.name}`,
                },
            }),
            call: (args) => args.inputs,
        });
    });

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

    it("enables versioning on production buckets", (done) => {
        infra.dataBucket.versioning.apply(versioning => {
            expect(versioning?.enabled).toBe(true);
            done();
        });
    });

    it("blocks public access by default", (done) => {
        infra.dataBucket.acl.apply(acl => {
            expect(acl).toBe("private");
            done();
        });
    });
});

Critical pattern: Import your infrastructure module after setting up mocks. The beforeAll/beforeEach pattern ensures mocks are active before Pulumi resources are instantiated.

Python with pytest

import unittest
import pulumi

class InfraMocks(pulumi.runtime.Mocks):
    def new_resource(self, args: pulumi.runtime.MockResourceArgs):
        state = {
            **args.inputs,
            "arn": f"arn:aws:s3:::{args.inputs.get('bucket', args.name)}",
        }
        return [f"{args.name}_id", state]

    def call(self, args: pulumi.runtime.MockCallArgs):
        return {}

pulumi.runtime.set_mocks(InfraMocks(), preview=False)

# Import AFTER mocks are set
from infra import data_bucket

class TestS3Bucket(unittest.TestCase):
    @pulumi.runtime.test
    def test_versioning_enabled(self):
        def check_versioning(versioning):
            self.assertTrue(versioning.get("enabled"))
        return data_bucket.versioning.apply(check_versioning)

    @pulumi.runtime.test
    def test_encryption_configured(self):
        def check_encryption(rules):
            self.assertIsNotNone(rules)
            self.assertGreater(len(rules), 0)
        return data_bucket.server_side_encryption_configuration.apply(
            lambda c: check_encryption(c.rules) if c else self.fail("No encryption")
        )

The @pulumi.runtime.test decorator handles Pulumi’s async Output types, making assertions cleaner.

Property Tests with CrossGuard

Property tests enforce invariants during deployment. Unlike unit tests that run in isolation, property tests see the actual resource graph Pulumi builds.

import * as policy from "@pulumi/policy";

const securityPolicies = new policy.PolicyPack("security", {
    policies: [
        {
            name: "s3-no-public-read",
            description: "S3 buckets must not allow public read access",
            enforcementLevel: "mandatory",
            validateResource: policy.validateResourceOfType(
                aws.s3.Bucket,
                (bucket, args, reportViolation) => {
                    if (bucket.acl === "public-read" ||
                        bucket.acl === "public-read-write") {
                        reportViolation("S3 bucket has public read access");
                    }
                }
            ),
        },
        {
            name: "ec2-approved-instance-types",
            description: "EC2 instances must use approved types",
            enforcementLevel: "mandatory",
            validateResource: policy.validateResourceOfType(
                aws.ec2.Instance,
                (instance, args, reportViolation) => {
                    const approved = ["t3.micro", "t3.small", "t3.medium"];
                    if (!approved.includes(instance.instanceType)) {
                        reportViolation(
                            `Instance type ${instance.instanceType} not approved. ` +
                            `Use: ${approved.join(", ")}`
                        );
                    }
                }
            ),
        },
    ],
});

Run policies with: pulumi up --policy-pack ./policy

When policies shine: Enforcing organization-wide standards across all stacks. One policy pack, every team benefits.

Integration Testing with Automation API

When you need to verify actual cloud behavior—not just configuration—use integration tests with Pulumi’s Automation API.

import { LocalWorkspace } from "@pulumi/pulumi/automation";
import * as aws from "@aws-sdk/client-s3";

describe("S3 infrastructure", () => {
    let stack: any;
    const stackName = `test-${Date.now()}`;

    beforeAll(async () => {
        stack = await LocalWorkspace.createOrSelectStack({
            stackName,
            projectName: "s3-test",
            program: async () => {
                const bucket = new aws.s3.Bucket("test-bucket", {
                    versioning: { enabled: true },
                });
                return { bucketName: bucket.id };
            },
        });

        await stack.setConfig("aws:region", { value: "us-west-2" });
        await stack.up({ onOutput: console.log });
    }, 300000); // 5 minute timeout

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

    it("creates a bucket with versioning", async () => {
        const outputs = await stack.outputs();
        const s3Client = new aws.S3Client({ region: "us-west-2" });

        const versioning = await s3Client.send(
            new aws.GetBucketVersioningCommand({
                Bucket: outputs.bucketName.value,
            })
        );

        expect(versioning.Status).toBe("Enabled");
    });
});

Warning: Integration tests provision real resources. Always:

  • Use unique stack names with timestamps
  • Implement proper cleanup in afterAll
  • Set appropriate timeouts (cloud operations are slow)
  • Run in isolated AWS accounts or with budget alerts

AI-Assisted Approaches

In 2026, AI tools accelerate infrastructure testing significantly. Here’s where they excel.

What AI does well:

  • Generating mock configurations from resource schemas
  • Writing property validation rules from security requirements
  • Creating test data variations for edge cases
  • Suggesting assertions you might have missed

What still needs humans:

  • Deciding which resources are critical to test
  • Understanding business context for validation rules
  • Reviewing AI-generated tests for false confidence
  • Architecting the overall testing strategy

Useful prompt for generating unit tests:

Given this Pulumi resource definition:

const bucket = new aws.s3.Bucket("data-bucket", {
    versioning: { enabled: true },
    serverSideEncryptionConfiguration: {
        rule: {
            applyServerSideEncryptionByDefault: {
                sseAlgorithm: "aws:kms",
            },
        },
    },
    lifecycleRules: [{
        enabled: true,
        transitions: [{ days: 30, storageClass: "STANDARD_IA" }],
    }],
});

Generate Jest unit tests that verify:
1. Versioning is enabled
2. KMS encryption is configured
3. Lifecycle transitions are set correctly

Use Pulumi's setMocks pattern and handle Output types properly.

Decision Framework

When to Use Unit Tests

This approach works best when:

  • You have complex conditional logic in resource creation
  • Multiple environments share infrastructure code with different configs
  • Your team already uses TypeScript/Python testing frameworks
  • You need fast feedback in CI (< 1 minute)

Consider alternatives when:

  • Infrastructure is purely declarative with no logic
  • You’re prototyping and expect frequent changes
  • The team is new to Pulumi (start with property tests)

When to Use Property Tests

This approach works best when:

  • Enforcing security/compliance across all stacks
  • You have organization-wide standards to maintain
  • Teams operate independently but share requirements
  • You need deployment-time guardrails

Consider alternatives when:

  • Policies are stack-specific (use unit tests)
  • You need to test actual cloud behavior (use integration)
  • Your policies require external data (complex)

When to Use Integration Tests

This approach works best when:

  • Testing interactions between cloud services
  • Validating IAM permissions work as expected
  • Verifying network connectivity and DNS resolution
  • Pre-production smoke tests

Consider alternatives when:

  • You need fast feedback (use unit tests)
  • You’re testing configuration, not behavior (use unit tests)
  • Cost or time constraints are tight

Measuring Success

MetricBaselineTargetHow to Track
Unit test coverage0%80%+ of conditional logicCoverage tools (nyc, coverage.py)
Unit test runtimeN/A< 60 secondsCI pipeline metrics
Property test violationsUnknown0 in productionPolicy pack reports
Integration test flakinessHigh< 5% failure rateCI failure analysis
Mean time to detect issuesHours/daysMinutesIncident tracking

Warning signs it’s not working:

  • Unit tests pass but deployments fail—mocks don’t match reality
  • Integration tests take > 30 minutes—too slow for meaningful feedback
  • Property violations discovered in production—policies aren’t comprehensive
  • High test maintenance burden—over-specified tests

Common Pitfalls

1. Testing Implementation, Not Behavior

// Bad: Testing internal structure
expect(bucket.tags).toEqual({ Name: "my-bucket", Env: "prod" });

// Good: Testing meaningful behavior
expect(bucket.versioning?.enabled).toBe(true);

2. Forgetting Output Unwrapping

Pulumi resources return Output<T>, not T. Always use .apply() or the test decorator.

# This will fail silently
def test_bad(self):
    self.assertEqual(bucket.arn, "expected-arn")  # Comparing Output object!

# This works
@pulumi.runtime.test
def test_good(self):
    return bucket.arn.apply(lambda arn: self.assertIn("s3", arn))

3. Incomplete Mocks

If your tests pass but deployments fail, your mocks might not match real provider behavior.

newResource: (args) => {
    // Realistic: include computed fields
    if (args.type === "aws:s3/bucket:Bucket") {
        return {
            id: args.name,
            state: {
                ...args.inputs,
                arn: `arn:aws:s3:::${args.inputs.bucket}`,
                bucketDomainName: `${args.inputs.bucket}.s3.amazonaws.com`,
                region: "us-west-2",
            },
        };
    }
    // Generic fallback
    return { id: args.name, state: args.inputs };
}

What’s Next

Start small: pick one critical resource (your production database, main S3 bucket, or VPC configuration) and write three unit tests for it. Use the Jest/pytest patterns above.

Once comfortable, add a policy pack with your top security requirement. That’s shift-left in action—catching issues before they reach production.


Related articles:

External resources: