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 Type | Speed | What It Validates | When It Runs |
|---|---|---|---|
| Unit Tests | ~20 seconds | Logic, configuration, resource properties | Every commit |
| Property Tests | During deployment | Policy compliance, security rules | pulumi up |
| Integration Tests | 5-30 minutes | Real cloud behavior, end-to-end flows | Pre-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
| Metric | Baseline | Target | How to Track |
|---|---|---|---|
| Unit test coverage | 0% | 80%+ of conditional logic | Coverage tools (nyc, coverage.py) |
| Unit test runtime | N/A | < 60 seconds | CI pipeline metrics |
| Property test violations | Unknown | 0 in production | Policy pack reports |
| Integration test flakiness | High | < 5% failure rate | CI failure analysis |
| Mean time to detect issues | Hours/days | Minutes | Incident 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:
- Terraform Testing and Validation Strategies
- Test Automation Pyramid Strategy
- CloudFormation Template Testing
- Test Reporting in CI/CD
External resources: