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>=7 and pytest-asyncio>=0.21 (Output resolution is async).
  • A pytest.ini or pyproject.toml setting asyncio_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.