Skip to content

Terraform Testing

How we test infrastructure code using the native Terraform testing framework

Related Concepts: Unit Testing | Backend Integration Testing | Terraform Project Structure

This guide covers testing with the Native Terraform Testing Framework. Write tests that feel natural to the framework — use run blocks, assert blocks, and mock_provider as Terraform intended them.

Test Levels

In the context of Terraform, we define four levels of testing:

Unit Tests

Unit tests do not require a cloud provider API key, login, or any real infrastructure being deployed. They use mock_provider exclusively and live in <root-module>/tests/unit/. This includes both command = plan tests that assert against planned values and command = apply tests that verify a mock apply succeeds. If it uses a mock provider, it's a unit test — regardless of whether it plans or applies.

Integration Tests

Integration tests require real infrastructure to be deployed and subsequently torn down. They live in <root-module>/tests/integration/ and run with real AWS credentials. Keep these thin — they incur real costs, and depending on the module's behavior, those costs can be significant. We prefer mocked unit tests over integration tests for this reason, though this opinion may change as tooling matures.

End-to-End Tests

E2E tests operate above the module level — tests at the root module level that verify the interaction and infrastructure lifecycle between multiple modules in tandem within a single root module.

Environment E2E Tests

Environment E2E tests exercise multiple root modules as they relate to the account-based folder structure. Each account (dev, staging, prod) contains multiple root modules with dependencies defined via terraform_remote_state data sources. An environment E2E test walks the root module dependency tree sequentially: apply admin-global (shared resources), then cluster-infra, then service-backend, service-worker, etc. in parallel — mirroring the real deployment order.

E2E and environment E2E tests are the most expensive and complex to maintain. They should be added deliberately and only when the interaction between modules or root modules is itself the thing being validated.

What to Test

Only assert on logic that lives in the module itself, not underlying Terraform behavior. Test the opinions your module encodes:

  • Conditional dynamic blocks (e.g. block present/absent based on input)
  • Action selection via dynamic blocks
  • Naming conventions from string interpolation
  • Tag merging logic
  • Correct number of resources from for_each / dynamic

Writing Unit Tests

Use mock_provider "aws" {} and module { source = "../../modules/<name>" } to test the module in isolation. For command = plan tests, assert against planned resource attributes. For command = apply tests, use mock_resource defaults to provide valid ARNs — the mock provider generates random strings that fail AWS ARN validation (see hashicorp/terraform-provider-aws#42834). Do not assert against hardcoded mock values; the apply succeeding is the assertion.

Writing Integration Tests

Use real providers with real credentials. Keep the scope narrow — test only what can't be validated with a mock provider (e.g. IAM policy evaluation, cross-service interactions, provider-specific validation that mocks can't replicate). Always ensure resources are torn down. Be mindful of costs.

CI Integration

The CI discovers root module directories and runs:

  • Fast checks: terraform test -test-directory=tests/unit (no AWS credentials)
  • Slow checks: terraform test -test-directory=tests/integration (real AWS credentials, skipped if directory doesn't exist)

Because these are non-default test directories, terraform init must include -test-directory=tests/unit (or tests/integration) so it discovers and installs modules referenced in test files.