Appearance
Terraform Testing
How we test infrastructure code using the native Terraform testing framework
Related Concepts: Terraform Change Control | 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, using 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, 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 and reach for a mocked unit test whenever a mock can cover the behavior. Terraform Change Control covers the cost that drives this preference.
End-to-End Tests
E2E tests operate above the module level. They sit at the root module level and 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.
Add E2E and environment E2E tests deliberately, and only when the interaction between modules or root modules is itself the thing being validated.
What to Test
Assert only on logic that lives in the module itself, leaving underlying Terraform behavior alone. 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, since 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 and test only what a mock provider cannot validate (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.