Skip to main content

This library provides snapshot testing for Terraform and OpenTofu modules. It generates and compares JSON snapshots of your planned infrastructure changes without deploying anything. Tests run using read-only credentials against provider APIs, giving you fast feedback on whether your code produces the infrastructure you expect.

Project description

Testing Terraform and OpenTofu code is difficult. The typical approach requires deploying infrastructure to verify correctness, which is slow, expensive, and blocks other developers. Native Terraform testing tools either require spinning up real resources or are limited in what they can validate.

This library provides snapshot testing for Terraform and OpenTofu modules. It generates and compares JSON snapshots of your planned infrastructure changes without deploying anything. Tests run using read-only credentials against provider APIs, giving you fast feedback on whether your code produces the infrastructure you expect.

GitHub project home

Table of Contents

Terraform / OpenTofu Unit Testing is complex and brittle

Following Martin Fowler’s The Practical Test Pyramid, unit tests should be fast, numerous, and cheap to run. Traditional Terraform testing does not allow for rapid and cheap iterations when developing complex infrastructure.

Current approaches are problematic:

  • Native Terraform tests require deploying real infrastructure, making them slow and costly;

  • Manual plan reviews catch issues late and don’t prevent regressions;

  • No tests at all means finding problems only after deployment to non-prod or production environments;

This creates practical issues:

  • Developers wait minutes or hours for feedback on their changes;

  • Testing blocks access to shared state files, preventing parallel development;

  • Infrastructure changes risk unexpected modifications without pre-deployment verification;

  • Teams spend significant time and money deploying test infrastructure just to validate code;

Snapshot Testing of Terraform / OpenTofu as Unit Testing

This library treats Terraform plans as unit test artifacts. You create test stacks that exercise your modules with different configurations, generate snapshots of the planned changes, and commit those snapshots to version control.

On every commit, the test suite verifies that your code still produces the expected plan. Changes to the planned infrastructure surface immediately in code review as snapshot diffs, making unintended consequences visible before any deployment.

This does not replace deployment testing in non-production environments. You still need to deploy and validate that your infrastructure actually works. Snapshot testing happens before deployment, catching problems earlier and more frequently:

  • Run snapshot tests on every commit (seconds);

  • Deploy to non-prod environment (minutes to hours);

  • Run integration tests and validate behavior;

  • Deploy to production;

The value is in the feedback loop. Snapshot tests catch configuration errors, unintended resource changes, and broken module logic immediately—problems that would otherwise only surface after waiting for a deployment. This means fewer failed deployments, faster iteration, and earlier detection of regressions.

Tests run in seconds using only read-only provider credentials. No state files, no deployed resources, no waiting.

You can find this library in this pypi repository.

Implementation

This module introduces the concept of Terraform snapshot unit testing. Testing your code can give you faster feedback cycles and guard you against unwanted changes. Snapshot tests are useful when you want to make sure your infrastructure does not change unexpectedly.

This approach still needs to be discussed as part of the wider team, but here’s how it works:

  1. You work on the Terraform module as usual. In the tests/ folder within the module, you create one or more test stacks which will produce different desired instantiations of the module. Once you’re happy witht the module and test instances, you then generate the snapshot using the pytest -m terraform --snapshot-update -s command;

  2. This will then intialise the Terraform modules, and use read-only credentials to the software provider (e.g AWS, GitLab, GitHub) to create a test plan. The synthetesis and planned values of the plan are then persisted as a snapshot of the test, without touching any infrastructure or state;

  3. You can visually inspect the generated manifests and plans in json, to verify that certain code conditions and resources exist in the way intended by the developer. In future releases, this verification will be done programatically using Syrupy and customisable YAML (e.g. verify that a specific object type with specific settings exists in a specific Terraform address);

  4. This snapshot testing can be executed at every push, to ensure that the intent of the developer is explictly captured in the test;

Advantages

Fast feedback cycles:

  • Tests run in seconds, not minutes or hours;

  • Developers get immediate feedback on code changes;

  • No waiting for infrastructure deployment or destruction;

No infrastructure costs:

  • Uses read-only provider credentials for planning only;

  • No actual resources created during testing;

  • No state files to manage or clean up;

Prevents unintended changes:

  • Snapshot diffs make all infrastructure changes explicit;

  • Unexpected modifications surface immediately in code review;

  • Guards against regressions when refactoring modules;

Enables parallel development:

  • No shared state file contention between developers;

  • Multiple team members can test simultaneously;

  • No blocking on deployment environments;

Encourages better module design:

  • Forces developers to write truly modular, reusable code;

  • Modules must accept configuration through variables which may be static or dynamic references;

  • Dependencies can be injected or coupled to remote state, depending on use-case;

  • Results in cleaner, more maintainable infrastructure code;

Integrates with existing workflows:

  • Runs as part of standard CI/CD pipelines;

  • Uses familiar pytest framework and conventions;

  • Snapshot diffs appear in pull request reviews like any other code change;

Complements deployment testing:

  • Catches configuration errors before expensive deployments;

  • Reduces failed deployment attempts;

  • Narrows the scope of issues found in non-prod environments;

Limitations

Cannot test composed infrastructure

  • Snapshot testing works well for isolated modules;

  • Testing multiple stacks that reference each other’s outputs is difficult;

  • Cross-stack dependencies require remote state, which this approach bypasses;

  • Complex multi-stack compositions still need integration testing via deployment;

Limited to plan validation:

  • Only validates what Terraform intends to create;

  • Cannot verify that infrastructure actually works as expected;

  • Does not catch provider-specific issues or API behavior;

  • Cannot test runtime behavior or integration between services;

Requires disciplined module design:

  • Modules must be written with dependency injection in mind;

  • Tightly coupled modules cannot be tested in isolation;

  • Teams need to adopt modular patterns consistently;

Snapshot maintenance overhead:

  • Snapshots must be updated when intentional changes occur;

  • Reviewing snapshot diffs requires understanding Terraform plan JSON;

  • False positives from provider version updates or irrelevant changes;

Does not replace other testing:

  • Still need deployment to non-prod for integration testing;

  • Still need manual verification of deployed infrastructure;

  • Still need production-like testing for performance and reliability;

  • This is one layer in a comprehensive testing strategy, not the entire strategy;

Limited programmatic validation:

  • Current implementation focuses on snapshot comparison;

  • Verifying specific resource configurations requires manual inspection;

  • Automated assertion of specific properties is planned but not yet implemented;

  • Cannot easily test conditional logic or complex module behavior;

Usage

Simple Example

  1. In the root folder of the Terraform / OpenTofu module, create a pytest.ini and customise environment variables based on your use-case and CI job (AWS example below):

    [pytest]
    markers =
        terraform: test Terraform / Tofu code
        order: order the tests
    addopts = --snapshot-warn-unused
    pythonpath = .
    env =
        TF_TEST_CMD=tofu
        AWS_DEFAULT_REGION=eu-west-1
        ADDITIONAL_TF_OVERRIDE_LOCATIONS=../
  2. Create a tests folder in the Terraform / OpenTofu module, and copy (or link if in composed repository) the test helpers (AWS tests example below):

    mkdir tests/
    cd tests/
    ln -s ../../../etc/tests-helpers/* .
  3. Overwrite your software providers (AWS provider example) to prevent interaction with the state backend, and create the test stack with the instantiations needed (AWS test stack example);

    # Go back to the Terraform / OpenTofu module root
    cd ..
    
    # Install the environment
    python3.12 -m venv .venv
    source .venv/bin/activate
    pip install -r tests/requirements-test.txt
  4. After making changes to you your module and reflecting these in the test instances, generate the snapshot:

    pytest -m terraform --snapshot-update -s
  5. This is an example of the expected output:

    ============================================================== test session starts ===============================================================
    platform linux -- Python 3.12.3, pytest-8.4.2, pluggy-1.6.0
    rootdir: /home/user/workspace/github/terraform-snapshot-test/tests/aws-s3-bucket
    configfile: pytest.ini
    plugins: syrupy-5.0.0, order-1.3.0, env-1.1.5
    collected 2 items
    
    tests/test_terraform_snapshot.py
    Initializing the backend...
    Initializing modules...
    
    Initializing provider plugins...
    - terraform.io/builtin/terraform is built in to OpenTofu
    - Reusing previous version of hashicorp/aws from the dependency lock file
    - Using previously-installed hashicorp/aws v6.15.0
    
    ╷
    │ Warning: Backend configuration ignored
    │
    │   on ../config.tf line 2, in terraform:
    │    2:   backend "s3" {}
    │
    │ Any selected backend applies to the entire configuration, so OpenTofu expects provider configurations only in the root module.
    │
    │ This is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing
    │ purposes, but this backend configuration block will have no effect.
    │
    │ (and one more similar warning elsewhere)
    ╵
    
    OpenTofu has been successfully initialized!
    
    You may now begin working with OpenTofu. Try running "tofu plan" to see
    any changes that are required for your infrastructure. All OpenTofu commands
    should now work.
    
    If you ever set or change modules or backend configuration for OpenTofu,
    rerun this command to reinitialize your working directory. If you forget, other
    commands will detect it and remind you to do so if necessary.
    ╷
    │ Warning: Backend configuration ignored
    │
    │   on ../config.tf line 2, in terraform:
    │    2:   backend "s3" {}
    │
    │ Any selected backend applies to the entire configuration, so OpenTofu expects provider configurations only in the root module.
    │
    │ This is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing
    │ purposes, but this backend configuration block will have no effect.
    ╵
    Success! The configuration is valid, but there were some validation warnings as shown above.
    module.stack_test_static_variable.data.aws_caller_identity.deployment_account: Reading...
    module.stack_test_static_variable.data.aws_caller_identity.deployment_account: Read complete after 0s [id=188415274210]
    module.stack_test_static_variable.data.aws_caller_identity.target_account: Reading...
    module.stack_test_static_variable.data.aws_caller_identity.target_account: Read complete after 0s [id=188415274210]
    
    OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
    + create
    <= read (data resources)
    
    OpenTofu will perform the following actions:
    
    # module.stack_test_static_variable.data.aws_iam_policy_document.storage will be read during apply
    # (config refers to values not yet known)
    <= data "aws_iam_policy_document" "storage" {
        + id            = (known after apply)
        + json          = (known after apply)
        + minified_json = (known after apply)
    
        + statement {
            + actions   = [
                + "s3:GetObject",
                + "s3:ListBucket",
                ]
            + resources = [
                + (known after apply),
                + (known after apply),
                ]
    
            + principals {
                + identifiers = [
                    + "arn:aws:iam::111111111111:role/lucille",
                    ]
                + type        = "AWS"
                }
            }
        }
    
    # module.stack_test_static_variable.aws_s3_bucket.storage will be created
    + resource "aws_s3_bucket" "storage" {
        + acceleration_status         = (known after apply)
        + acl                         = (known after apply)
        + arn                         = (known after apply)
        + bucket                      = (known after apply)
        + bucket_domain_name          = (known after apply)
        + bucket_prefix               = (known after apply)
        + bucket_region               = (known after apply)
        + bucket_regional_domain_name = (known after apply)
        + force_destroy               = false
        + hosted_zone_id              = (known after apply)
        + id                          = (known after apply)
        + object_lock_enabled         = (known after apply)
        + policy                      = (known after apply)
        + region                      = "eu-west-1"
        + request_payer               = (known after apply)
        + tags_all                    = {
            + "cost_center" = "1979"
            + "environment" = "joe's garage"
            + "owner"       = "frank zappa"
            }
        + website_domain              = (known after apply)
        + website_endpoint            = (known after apply)
    
        + cors_rule (known after apply)
    
        + grant (known after apply)
    
        + lifecycle_rule (known after apply)
    
        + logging (known after apply)
    
        + object_lock_configuration (known after apply)
    
        + replication_configuration (known after apply)
    
        + server_side_encryption_configuration (known after apply)
    
        + versioning (known after apply)
    
        + website (known after apply)
        }
    
    # module.stack_test_static_variable.aws_s3_bucket_policy.storage will be created
    + resource "aws_s3_bucket_policy" "storage" {
        + bucket = (known after apply)
        + id     = (known after apply)
        + policy = (known after apply)
        + region = "eu-west-1"
        }
    
    Plan: 2 to add, 0 to change, 0 to destroy.
    ╷
    │ Warning: Backend configuration ignored
    │
    │   on ../config.tf line 2, in terraform:
    │    2:   backend "s3" {}
    │
    │ Any selected backend applies to the entire configuration, so OpenTofu expects provider configurations only in the root module.
    │
    │ This is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing
    │ purposes, but this backend configuration block will have no effect.
    ╵
    
    ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    
    Saved the plan to: __snapshots__/_1759855219.plan
    
    To perform exactly these actions, run the following command to apply:
        tofu apply "__snapshots__/_1759855219.plan"
    ..
    
    ------------------------------------------------------------ snapshot report summary -------------------------------------------------------------
    2 snapshots passed. 2 unused snapshots deleted.
    
    Deleted unknown snapshot collection (tests/__snapshots__/_1759855219.plan)
    Deleted unknown snapshot collection (tests/__snapshots__/_1759855219.json)
    =============================================================== 2 passed in 6.19s ================================================================
  6. This will generate the snapshots with the module synthesis and planned values for the different tests, which will be committed to the repository;

  7. To run these unit tests as part of the CI/CD pipeline, you could then run the following command from the Terraform / OpenTofu root, and verify that code being built meets the expected state as defined and verified by the engineer as per the snapshot:

    pytest
  8. Example of the output of the test comparison with the snapshots:

    ============================================================== test session starts ===============================================================
    platform linux -- Python 3.12.3, pytest-8.4.2, pluggy-1.6.0
    rootdir: /home/user/workspace/github/terraform-snapshot-test/tests/aws-s3-bucket
    configfile: pytest.ini
    plugins: syrupy-5.0.0, order-1.3.0, env-1.1.5
    collected 2 items
    
    tests/test_terraform_snapshot.py ..                                                                                                        [100%]
    
    ------------------------------------------------------------ snapshot report summary -------------------------------------------------------------
    2 snapshots passed. 4 snapshots unused.
    
    Re-run pytest with --snapshot-update to delete unused snapshots.
    =============================================================== 2 passed in 6.11s ================================================================

Expectations Example

If you want to extend the use of expectations, you can create an assertions folder in the tests folder of the terraform stack.

In this folder, you can create one YAML file per stack which you want to test. Here’s the syntax of the expectations YAML file:

module: module.stack_test_static_variable
description: Test stack static variable assertions

synthesis:
    assertions: {}

planned_values:
    assertions: {}

In the expecations section you can write the objects and configuration which you want to ensure will exist in the synthesis and planned_values snapshots.

You can also specificy the following macros / key words as part of the assertion criteria:

  • $MODULE: Replaced at runtime by the module name (taken from the top of the assertions file);

  • $NOTNULL: When matching a scalar property, ensures the value of the property is non-null;

See here two examples of expecations:

Running the Examples

To run the examples you need to have read-only access to the relevant APIs:

  • Simple example of AWS S3 Bucket, with static dependency variables (and commented examples of referended and remote state dependencies);

  • Simple example of GitLab Project, with with static dependency variables (and commented examples of referended and remote state dependencies);

  • Simple example of GitHub Repository, with static dependency variables;

  • Expectations example of AWS Transit Gateway and AWS Network Firewall, with static dependency variables and expectations for non-prod (without firewall) and prod (with firewall);

Future Work

  • Add more examples of different providers;

  • Any other relevant requests by the community;

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

terraform_snapshot_test-0.0.3.tar.gz (19.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

terraform_snapshot_test-0.0.3-py3-none-any.whl (12.6 kB view details)

Uploaded Python 3

File details

Details for the file terraform_snapshot_test-0.0.3.tar.gz.

File metadata

  • Download URL: terraform_snapshot_test-0.0.3.tar.gz
  • Upload date:
  • Size: 19.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for terraform_snapshot_test-0.0.3.tar.gz
Algorithm Hash digest
SHA256 0fc6019fc4fc16a926f9a23e7922b0518953d50e61751b42cf2246906f4e7193
MD5 caf5b9652f26c11a8da77aabbf318322
BLAKE2b-256 42aa12cb80a85572d313579bc56c9a726dd287a014f7f85f02ad81903a4e3e6f

See more details on using hashes here.

File details

Details for the file terraform_snapshot_test-0.0.3-py3-none-any.whl.

File metadata

File hashes

Hashes for terraform_snapshot_test-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 ab3c188d1c993a1c575c0fbe1a328755d2e6343493eebec811472cc7d82d27c2
MD5 109f58ca6b38c52a2929729855ed1fe9
BLAKE2b-256 1a8528262d57cb5a1f9541798c966e8c91a37ed890f20b99c3d7ab1766e90078

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page