Unit Testing Pulumi Programs with Mocks
Unit testing a Pulumi program with pulumi.runtime.set_mocks runs your resource graph in-process with the cloud provider replaced by a stub, letting you assert on resource inputs and outputs in milliseconds without credentials or state. This how-to belongs to Testing Python Infrastructure Code within Python IaC fundamentals and strategy, and it is the fastest, lowest layer of the testing pyramid for Pulumi.
Context
A Pulumi program builds a graph of resources whose values are wrapped in Output and resolved asynchronously against cloud APIs. Mocks intercept that resolution: every resource registration returns a fake id and a dictionary of state you control, so you can verify that your code passes the right inputs and wires outputs correctly. Because nothing reaches a provider, these tests are deterministic and need no AWS account — the same approach the Pulumi patterns and provider management section uses to test its component resources.
Prerequisites
- Python 3.9+,
pulumi>=3.0, and the relevant provider package (e.g.pulumi_aws>=6). pytest>=7andpytest-asyncio>=0.21(Output resolution is async).- A
pytest.iniorpyproject.tomlsettingasyncio_mode = "auto"or per-test@pytest.mark.asyncio. - No cloud credentials — mocks never call a provider.
Implementation
1. Define a Mocks subclass and install it before importing infra code
set_mocks must run before the module under test constructs any resource, so install it in an autouse fixture or at import time.
# CLI: pytest tests/test_bucket.py -v
# State implication: new_resource returns fabricated state; no real bucket, no state file.
from typing import Any
import pulumi
class InfraMocks(pulumi.runtime.Mocks):
def new_resource(
self, args: pulumi.runtime.MockResourceArgs
) -> tuple[str, dict[str, Any]]:
outputs = {**args.inputs}
# Provider note: fabricate provider-computed fields the program reads downstream.
if args.typ == "aws:s3/bucket:Bucket":
outputs["arn"] = f"arn:aws:s3:::{args.name}"
return f"{args.name}-id", outputs
def call(self, args: pulumi.runtime.MockCallArgs) -> dict[str, Any]:
return {} # stub provider function (data source) calls
pulumi.runtime.set_mocks(InfraMocks(), preview=False)
2. Resolve outputs and assert on inputs
With mocks installed, import the module that defines your resources and await its outputs through the Output future.
# CLI: pytest tests/test_bucket.py::test_bucket_is_versioned -v
import pulumi
import pulumi_aws as aws
import pytest
def make_bucket(name: str) -> aws.s3.Bucket:
return aws.s3.Bucket(name, versioning={"enabled": True})
@pytest.mark.asyncio
async def test_bucket_is_versioned() -> None:
bucket = make_bucket("logs")
def check(args: list[Any]) -> None:
enabled, arn = args
assert enabled is True # asserts the input we passed
assert arn.startswith("arn:aws:s3:::") # asserts mocked output
pulumi.Output.all(
bucket.versioning.enabled, bucket.arn
).apply(check)
3. Toggle preview mode to test plan-time behavior
Pass preview=True to set_mocks when you need to verify behavior under pulumi preview, where some outputs are unknown.
# CLI: pytest tests/test_preview.py -v
import pulumi
pulumi.runtime.set_mocks(InfraMocks(), preview=True)
# State implication: under preview, computed outputs may be unknown — guard apply() accordingly.
Verification
Run the suite; a passing run proves the resource inputs and the mocked outputs match your expectations without any provider call.
# CLI: confirm Pulumi unit tests are hermetic and green
pytest tests/ -q
# Provider note: no PULUMI_ACCESS_TOKEN or AWS creds required for this layer.
Gotchas & Edge Cases
Assertions inside apply can be swallowed. If a test only registers an apply callback and returns, pytest may finish before the callback runs. Prefer awaiting through pytest-asyncio and the Output future, or use pulumi.Output.all(...).apply(...) and ensure the test framework drives the event loop to completion.
set_mocks installed too late has no effect. Resources constructed at module import before the mock is installed register against the real runtime. Install mocks in conftest.py or before importing the infrastructure module.
call must be implemented for data sources. Programs that use aws.get_* functions will fail unless your Mocks call returns a dictionary matching the function's expected outputs.
Frequently Asked Questions
What is the difference between mocking Pulumi and mocking boto3 with moto?
pulumi.runtime.set_mocks stubs the Pulumi engine's resource registration, so it tests how your program declares infrastructure. moto stubs the AWS SDK, so it tests boto3 calls your helper code makes outside the Pulumi graph. Use both at the same layer of the pyramid for different code paths.
Can I assert that a resource was created with a specific name?
Yes. MockResourceArgs.name and args.inputs are available in new_resource, so you can record them into a list and assert on the captured calls after the program runs.
Do mocks work for ComponentResource subclasses?
Yes. Components register their children through the same runtime, so each child hits new_resource. This is exactly how component tests in the Pulumi patterns and provider management section verify nested resource graphs.
Related
- Testing Python Infrastructure Code — the testing pyramid this unit layer anchors.
- Mocking AWS Services with moto in pytest — mock the AWS SDK for boto3 calls outside the Pulumi graph.
- Pulumi Patterns & Provider Management — production component patterns that rely on this Mocks-based testing approach.