Your team wants to unit test and integration test Pulumi infrastructure code before deploying to real cloud environments. How do you implement a comprehensive testing strategy for Pulumi programs?
Quick Answer
Use Pulumi's mocking framework for unit tests to verify resource configurations without cloud calls, property tests to validate outputs, and integration tests that deploy real stacks to ephemeral environments. Layer these in CI/CD with unit tests on every PR and integration tests on merge to main.
Detailed Answer
Unit Testing with Mocks
Pulumi provides a mocking framework that intercepts resource creation calls and returns fake outputs. You define mock implementations that simulate cloud provider behavior without making any API calls. This lets you test resource configuration logic, conditional resource creation, and output transformations in milliseconds. In TypeScript, use pulumi.runtime.setMocks() in your test setup. In Python, use pulumi.runtime.set_mocks().
Property Testing
Property tests verify that the outputs of your Pulumi program meet certain invariants after the mock engine processes all resources. For example: all S3 buckets must have encryption enabled, all security groups must not allow 0.0.0.0/0 ingress on port 22, all RDS instances must have multi-AZ enabled in production. These are like unit tests but focus on compliance properties rather than specific resource configurations.
Integration Testing
Integration tests deploy real infrastructure to an ephemeral environment (e.g., a dedicated AWS account or namespace), verify the resources work correctly, and tear everything down. Pulumi's @pulumi/pulumi/automation API makes this programmatic: create a temporary stack, deploy it, run assertions against real cloud resources (check if the load balancer responds, verify database connectivity), then destroy the stack. These tests are slow (minutes) but catch provider-specific issues that mocks miss.
CI/CD Test Pipeline
Structure your pipeline in layers: unit tests and property tests run on every pull request (seconds). Integration tests run on merge to main against a staging environment (minutes). Canary deployments to production run after integration tests pass. Use pulumi preview as an additional check in PRs to detect potential issues without deploying.
Code Example
// === Unit Test with Mocks (Jest + TypeScript) ===
import * as pulumi from "@pulumi/pulumi";
// Set up mocks BEFORE importing the Pulumi program
pulumi.runtime.setMocks({
newResource: function(args: pulumi.runtime.MockResourceArgs): { id: string, state: any } {
return {
id: args.inputs.name + "-id",
state: {
...args.inputs,
arn: `arn:aws:s3:::${args.inputs.name || "test"}`,
},
};
},
call: function(args: pulumi.runtime.MockCallArgs) {
return args.inputs;
},
});
// Now import the infrastructure module
import { bucket, securityGroup } from "../index";
describe("Infrastructure", () => {
test("S3 bucket has encryption enabled", async () => {
const encryption = await new Promise<any>((resolve) =>
bucket.serverSideEncryptionConfiguration.apply(resolve)
);
expect(encryption).toBeDefined();
expect(encryption.rules[0].applyServerSideEncryptionByDefault.sseAlgorithm)
.toBe("aws:kms");
});
test("Security group does not allow SSH from anywhere", async () => {
const ingress = await new Promise<any[]>((resolve) =>
securityGroup.ingress.apply(resolve)
);
const sshRules = ingress.filter(r => r.fromPort === 22);
for (const rule of sshRules) {
expect(rule.cidrBlocks).not.toContain("0.0.0.0/0");
}
});
});
// === Policy/Property Test ===
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";
new PolicyPack("compliance", {
policies: [{
name: "s3-no-public-read",
description: "S3 buckets must not have public-read ACL",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, report) => {
if (bucket.acl === "public-read") {
report("S3 bucket must not be public-read");
}
}),
}],
});
// === Integration Test (Automation API) ===
import { LocalWorkspace } from "@pulumi/pulumi/automation";
import axios from "axios";
describe("Integration", () => {
let stack: any;
beforeAll(async () => {
stack = await LocalWorkspace.createOrSelectStack({
stackName: `test-${Date.now()}`,
workDir: "../",
});
await stack.setConfig("aws:region", { value: "us-west-2" });
await stack.up();
}, 300000);
test("ALB responds with 200", async () => {
const outputs = await stack.outputs();
const resp = await axios.get(outputs.albDnsName.value);
expect(resp.status).toBe(200);
});
afterAll(async () => {
await stack.destroy();
await stack.workspace.removeStack(stack.name);
}, 300000);
});Interview Tip
Testing is where Pulumi genuinely outshines Terraform. Show the three layers: fast unit tests with mocks, property/policy tests for compliance, and slow integration tests with real resources. Mention the CI/CD pyramid where unit tests gate PRs and integration tests gate merges.