Pulumi revolutionizes infrastructure testing by letting teams use familiar programming languages and native test frameworks instead of learning DSL-specific tools. According to a Pulumi State of Cloud Engineering 2022 survey, teams that implement comprehensive infrastructure testing reduce deployment failures by 45% and recover from incidents 60% faster than those relying on manual verification. According to research by Puppet in their State of DevOps Report 2023, organizations practicing infrastructure as code with automated testing see 5x higher deployment frequency and 3x lower change failure rates. For QA engineers and DevOps teams, Pulumi’s approach means unit tests with mocks run 60x faster than integration tests — one team cut their 20-minute suite down to 20 seconds — while property tests catch compliance violations before deployment happens.

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.

“Infrastructure testing with Pulumi follows the same quality standards as application testing — 60x faster unit tests mean there’s no excuse to skip them.” — Yuri Kan, Senior QA Lead

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:

Official Resources

FAQ

What makes Pulumi testing different from Terraform testing?

Pulumi uses native language test frameworks (Jest for TypeScript, pytest for Python) with mock infrastructure providers, so tests run in milliseconds without provisioning real resources. Terraform requires HCL-specific tools like Terratest. Pulumi’s mocking system intercepts resource creation calls, letting you verify configurations without cloud credentials or costs.

What are Pulumi unit tests and how do I write them?

Pulumi unit tests use pulumi.runtime.setMocks() to intercept resource creation and return mock values. Write tests in your native framework (Jest/pytest/go test), call your Pulumi program, and assert on resource properties. This verifies all resources before any infrastructure is provisioned.

What is CrossGuard in Pulumi?

CrossGuard is Pulumi’s policy-as-code engine that validates resources during pulumi up before provisioning. Define policies in TypeScript or Python using the @pulumi/policy SDK — rules check resource configurations and fail the deployment if violations occur.

When should I use integration tests vs unit tests in Pulumi?

Use unit tests (with mocks) for resource configuration validation, dependency graph checks, naming conventions, policy compliance — runs in seconds, no cloud costs. Use integration tests for actual provisioning verification. Aim for 90% unit / 10% integration split.

See Also