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;

Future Work

  • Add the verification of explicit object configuration using YAML manifests for the different test cases (e.g. verify that a specific object type with specific settings exists in a specific Terraform address);

  • Add more examples of different providers;

  • Any other relevant requests by the community;

Usage

  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 synthetesis 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 ================================================================

Tests

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

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

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

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

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.2.tar.gz (14.7 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.2-py3-none-any.whl (9.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: terraform_snapshot_test-0.0.2.tar.gz
  • Upload date:
  • Size: 14.7 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.2.tar.gz
Algorithm Hash digest
SHA256 a51986db6df43637fc779a4bf8797a69e122ce21ae62f063a562e68ad64bbd36
MD5 d006744ddc2ed31223107ea57ade39e9
BLAKE2b-256 3c6b0cc26bf3ff1fb425709a670a0d19b6370e7715e632832cecb450dd9bf131

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for terraform_snapshot_test-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 d41e3386c0d772ef5450699683bdd8f71a79bab611d16655a2279e0e5537afd7
MD5 8e4972daf45933e450f9a9001bcaf469
BLAKE2b-256 33a5e5c6acafd27a7539d5ef7d3eed907aaed30b74bd92e1d87e431fd9f380ee

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