terraform-snapshot-test added to PyPI

terraform-snapshot-test added to PyPI

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.

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.

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;

Stay Informed

Get the best articles every day for FREE. Cancel anytime.