Terraform and OpenTofu Testing: tofu test, validate and Terratest
Test your infrastructure code before it reaches production #
Infrastructure code deserves tests like any other code. The tests go from cheap and fast to expensive and realistic. Use the cheap ones on every commit and the expensive ones before releasing a module.
1. Format and validate #
$ tofu fmt -check -recursive
$ tofu init -backend=false
$ tofu validate
fmt checks the style and validate checks syntax and internal consistency (types, references) without contacting any cloud. Both take seconds.
2. Lint and security scan #
tflint finds errors that validate cannot, such as an invalid EC2 instance type. Security scanners find insecure configurations. See security scanning.
3. Native tests: terraform test and tofu test #
Terraform 1.6 and OpenTofu 1.6 added a built-in test framework. Test files end in .tftest.hcl (or .tofutest.hcl) and live next to the module or in a tests/ directory.
variables {
vpc_cidr = "10.10.0.0/16"
}
run "creates_vpc_with_expected_cidr" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.10.0.0/16"
error_message = "Unexpected VPC CIDR."
}
}
run "rejects_invalid_cidr" {
command = plan
variables {
vpc_cidr = "not-a-cidr"
}
expect_failures = [var.vpc_cidr]
}Run them:
$ tofu test
tests/network.tftest.hcl... in progress
run "creates_vpc_with_expected_cidr"... pass
run "rejects_invalid_cidr"... pass
tests/network.tftest.hcl... tearing down
tests/network.tftest.hcl... pass
Success! 2 passed, 0 failed.
command = planonly plans, which is fast and free.command = apply(the default) creates the real infrastructure, runs the assertions and destroys it at the end. Use a sandbox AWS account.expect_failureschecks that a validation, precondition or check fails when it should.
Mock providers #
Newer versions (Terraform 1.7 and OpenTofu 1.8 or later) can replace a provider with a mock, so the tests run without credentials or cost:
mock_provider "aws" {}
run "plan_with_mock" {
command = apply
assert {
condition = length(aws_subnet.private) == 3
error_message = "Expected 3 private subnets."
}
}Check the documentation of your version for the exact syntax and limitations of mocks.
4. Integration tests with Terratest #
Terratest is a Go library that applies the code, checks the real infrastructure (an HTTP request, an AWS API call) and destroys it:
func TestVpc(t *testing.T) {
opts := &terraform.Options{TerraformDir: "../examples/basic", TerraformBinary: "tofu"}
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
vpcID := terraform.Output(t, opts, "vpc_id")
assert.NotEmpty(t, vpcID)
}Use it when you need to test behavior, not just configuration.
Recommended pipeline #
| Stage | Tool | When |
|---|---|---|
| Format and validate | fmt, validate |
Every commit |
| Lint and security | tflint, Checkov, Trivy | Every pull request |
| Unit tests | test with plan or mocks |
Every pull request |
| Integration | test with apply or Terratest |
Before releasing a module, on a schedule |
Run them in GitHub Actions and read about debugging Terraform when a test fails.