Snapshot Testing CDKTF Stacks with pytest

Snapshot testing a CDKTF stack synthesizes it to Terraform JSON in-process and compares the result against a stored golden file, so any unintended change to the generated configuration fails the test before it ever reaches a plan. This how-to is part of Testing Python Infrastructure Code under Python IaC fundamentals and strategy, and it is the snapshot layer of the testing pyramid for CDKTF projects.

Context

CDKTF's architecture and synthesis step turns your Python constructs into Terraform JSON. Because that JSON is deterministic, it is an ideal subject for a snapshot test: synthesize once, save the output as a golden file, and on every later run fail if the new output differs. This catches accidental changes — a flipped flag, a renamed resource, a dropped tag — that compile and synthesize cleanly but alter what Terraform would apply. cdktf.Testing.synth runs synthesis without writing to disk or touching a provider, so the test stays fast and hermetic.

Prerequisites

  • Python 3.9+, cdktf>=0.20, and constructs>=10.
  • The generated provider bindings under .gen/ (run cdktf get first).
  • pytest>=7; no Terraform binary and no cloud credentials are needed for Testing.synth.
  • A committed tests/__snapshots__/ directory for golden files.

Implementation

1. Synthesize a stack in isolation with Testing.synth

Construct the stack inside a Testing.app() scope and call Testing.synth to get the Terraform JSON as a string.

# CLI: pytest tests/test_stack_snapshot.py -v
# State implication: Testing.synth runs in-memory; it writes no cdktf.out and locks no state.
import json
from cdktf import Testing, TerraformStack
from constructs import Construct
# Provider note: import provider bindings from .gen produced by `cdktf get`.
from imports.aws.provider import AwsProvider
from imports.aws.s3_bucket import S3Bucket

class StorageStack(TerraformStack):
    def __init__(self, scope: Construct, sid: str, *, bucket_name: str) -> None:
        super().__init__(scope, sid)
        AwsProvider(self, "aws", region="us-east-1")
        S3Bucket(self, "data", bucket=bucket_name, force_destroy=False)

def synth_storage(bucket_name: str) -> dict:
    app = Testing.app()
    stack = StorageStack(app, "storage", bucket_name=bucket_name)
    return json.loads(Testing.synth(stack))

2. Assert specific keys, or the whole golden file

For targeted checks, assert on a single resource block; for full coverage, compare the entire synthesized document against a snapshot.

# CLI: pytest tests/test_stack_snapshot.py::test_bucket_block -v
def test_bucket_block() -> None:
    synth = synth_storage("acme-data")
    bucket = synth["resource"]["aws_s3_bucket"]["data"]
    assert bucket["bucket"] == "acme-data"
    assert bucket["force_destroy"] is False  # guards against accidental data loss

3. Compare against a committed golden file

Store the first run's output and diff every later run against it; this is what catches drift in the generated configuration.

# CLI: pytest tests/test_stack_snapshot.py::test_full_snapshot -v
# CLI to refresh after an intentional change: UPDATE_SNAPSHOTS=1 pytest tests/ -q
import json, os
from pathlib import Path

SNAP = Path(__file__).parent / "__snapshots__" / "storage.json"

def test_full_snapshot() -> None:
    current = synth_storage("acme-data")
    if os.getenv("UPDATE_SNAPSHOTS"):
        SNAP.write_text(json.dumps(current, indent=2, sort_keys=True))
    expected = json.loads(SNAP.read_text())
    assert current == expected  # fails on any unintended synthesis change

Verification

Run the suite; a green run confirms the synthesized Terraform JSON matches the committed golden file exactly.

# CLI: run snapshot tests, then intentionally refresh after a reviewed change
pytest tests/ -q
UPDATE_SNAPSHOTS=1 pytest tests/ -q   # only when the diff is expected and reviewed
# Provider note: no Terraform binary invoked; this asserts synthesis output, not a plan.

Gotchas & Edge Cases

Non-deterministic values poison snapshots. Timestamps, random suffixes, or unsorted maps make the golden file differ on every run. Pin such inputs in tests and dump JSON with sort_keys=True so key ordering is stable.

A blanket snapshot update hides real regressions. Running UPDATE_SNAPSHOTS=1 blindly accepts whatever changed, defeating the test. Always read the diff first and only refresh when the change is intended and reviewed.

Stale .gen bindings change the output. If a teammate bumps a provider version and regenerates, the synthesized JSON shifts. Pin provider versions in cdktf.json and treat .gen regeneration as a deliberate, reviewed step so snapshots stay meaningful.

Frequently Asked Questions

How is a CDKTF snapshot test different from terraform plan? A snapshot test compares the synthesized Terraform JSON against a stored file with no provider or backend involved, so it is fast and hermetic. terraform plan evaluates that JSON against real state and provider APIs to compute changes. The snapshot catches configuration drift; the plan catches state drift.

Where should golden files live? Commit them under tests/__snapshots__/ alongside the tests so they are versioned and reviewed in pull requests. The diff on the golden file is the signal that something changed.

Can I snapshot test individual constructs instead of whole stacks? Yes. Wrap the construct in a throwaway TerraformStack inside Testing.app(), synthesize, and assert on its resource block — the same pattern used for reusable constructs in the CDKTF workflows and Terraform synthesis section.