A comprehensive comparison of Terraform's native test framework vs Gruntwork's Terratest. Covers mock providers, real infrastructure testing, CI/CD integration, and when to use each tool.
The Testing Dilemma Every Infrastructure Team Faces
You've written Terraform modules. Now you need to test them. In 2026, you have two mature options: HashiCorp's native `terraform test` framework (GA since Terraform 1.6) or Gruntwork's battle-tested Terratest Go library.
The choice isn't obvious — and getting it wrong means either slow, expensive test suites or insufficient coverage.
Terraform Test: The Native Approach
HashiCorp introduced terraform test in Terraform 1.6 (October 2023) and has rapidly evolved it. The current state in 2026 (Terraform 1.14+) includes mock providers, HCP Terraform integration, and enhanced debugging capabilities.
How It Works
Tests live in .tftest.hcl files alongside your modules:
1# tests/s3_bucket.tftest.hcl23# Variables available to all run blocks4variables {5 bucket_name = "test-bucket"6 environment = "test"7}89# Run block 1: Unit test (plan only, no infrastructure)10run "validate_bucket_configuration" {11 command = plan1213 assert {14 condition = aws_s3_bucket.main.versioning[0].enabled == true15 error_message = "Versioning must be enabled"16 }1718 assert {19 condition = contains(keys(aws_s3_bucket.main.tags), "Environment")20 error_message = "Environment tag is required"21 }22}2324# Run block 2: Integration test (deploys real infrastructure)25run "deploy_and_validate" {26 command = apply2728 assert {29 condition = output.bucket_arn != ""30 error_message = "Bucket ARN should not be empty"31 }32}Run tests with a single command:
1terraform test2# Or with verbose output3terraform test -verboseMock Providers: The Game Changer (Terraform 1.7+)
This is where native testing becomes powerful. Mock providers let you test without cloud credentials or costs:
1# tests/mocked.tftest.hcl23# Mock the entire AWS provider4mock_provider "aws" {5 mock_resource "aws_s3_bucket" {6 defaults = {7 id = "test-bucket-id"8 arn = "arn:aws:s3:::test-bucket"9 bucket = "test-bucket"10 }11 }1213 mock_resource "aws_instance" {14 defaults = {15 id = "i-mock12345"16 private_ip = "10.0.1.100"17 availability_zone = "ap-southeast-2a"18 }19 }2021 mock_data "aws_ami" {22 defaults = {23 id = "ami-mock12345"24 architecture = "x86_64"25 }26 }27}2829run "test_with_mocks" {30 command = plan3132 assert {33 condition = aws_instance.web.id == "i-mock12345"34 error_message = "Mock should provide predictable values"35 }36}Override Blocks for Surgical Mocking
Need to mock specific resources while keeping others real?
1# Override a specific resource2override_resource {3 target = aws_instance.database4 values = {5 id = "i-db-override"6 private_ip = "10.0.2.50"7 }8}910# Override a data source11override_data {12 target = data.aws_vpc.main13 values = {14 id = "vpc-override123"15 cidr_block = "10.0.0.0/16"16 }17}1819# Override an entire module's outputs20override_module {21 target = module.networking22 outputs = {23 vpc_id = "vpc-module123"24 subnet_ids = ["subnet-1", "subnet-2"]25 }26}Negative Testing: Expect Failures
Test that your validation rules work:
1run "test_invalid_instance_type" {2 command = plan34 variables {5 instance_type = "invalid-garbage"6 }78 # This test PASSES if the variable validation fails9 expect_failures = [10 var.instance_type11 ]12}1314run "test_missing_required_tag" {15 command = plan1617 variables {18 tags = {} # Missing required "Environment" tag19 }2021 expect_failures = [22 var.tags23 ]24}HCP Terraform Integration
Run tests in HCP Terraform (formerly Terraform Cloud):
1# Run tests remotely2terraform test -cloud-run=app.terraform.io/buun-group/aws-modules/s3Tests run automatically on commits when configured in your module settings.
Terratest: The Go-Powered Integration Framework
Terratest is a Go library by Gruntwork that deploys real infrastructure and validates it with actual API calls, HTTP requests, and SSH connections.
How It Works
1// test/s3_bucket_test.go2package test34import (5 "testing"6 "github.com/gruntwork-io/terratest/modules/terraform"7 "github.com/gruntwork-io/terratest/modules/aws"8 "github.com/stretchr/testify/assert"9)1011func TestS3Bucket(t *testing.T) {12 t.Parallel()1314 terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{15 TerraformDir: "../modules/s3-bucket",16 Vars: map[string]interface{}{17 "bucket_name": "test-bucket-" + random.UniqueId(),18 "environment": "test",19 },20 })2122 // ALWAYS clean up, even if test fails23 defer terraform.Destroy(t, terraformOptions)2425 // Deploy real infrastructure26 terraform.InitAndApply(t, terraformOptions)2728 // Get outputs29 bucketName := terraform.Output(t, terraformOptions, "bucket_name")30 bucketArn := terraform.Output(t, terraformOptions, "bucket_arn")3132 // Validate using AWS SDK33 bucket := aws.GetS3BucketVersioning(t, "ap-southeast-2", bucketName)34 assert.Equal(t, "Enabled", bucket.Status)3536 // More assertions37 assert.Contains(t, bucketArn, "arn:aws:s3")38}HTTP Endpoint Testing
Perfect for testing web servers, APIs, and load balancers:
1func TestWebServer(t *testing.T) {2 t.Parallel()34 terraformOptions := &terraform.Options{5 TerraformDir: "../modules/web-server",6 }78 defer terraform.Destroy(t, terraformOptions)9 terraform.InitAndApply(t, terraformOptions)1011 // Get the URL from Terraform output12 url := terraform.Output(t, terraformOptions, "url")1314 // Validate with retries (infrastructure takes time to stabilize)15 http_helper.HttpGetWithRetry(16 t,17 url,18 nil, // TLS config19 200, // Expected status20 "Hello, World!", // Expected body21 30, // Max retries22 10 * time.Second, // Time between retries23 )24}SSH Validation
Test that you can actually connect to instances:
1func TestSSHAccess(t *testing.T) {2 t.Parallel()34 terraformOptions := &terraform.Options{5 TerraformDir: "../modules/bastion",6 }78 defer terraform.Destroy(t, terraformOptions)9 terraform.InitAndApply(t, terraformOptions)1011 publicIP := terraform.Output(t, terraformOptions, "public_ip")12 keyPair := terraform.Output(t, terraformOptions, "private_key")1314 host := ssh.Host{15 Hostname: publicIP,16 SshKeyPair: keyPair,17 SshUserName: "ec2-user",18 }1920 // Run command via SSH21 output := ssh.CheckSshCommand(t, host, "echo 'Hello from Terratest'")22 assert.Contains(t, output, "Hello from Terratest")23}Multi-Tool Testing
Terratest shines when testing pipelines that span multiple tools:
1func TestFullStack(t *testing.T) {2 t.Parallel()34 // Step 1: Build AMI with Packer5 packerOptions := &packer.Options{6 Template: "../packer/app.pkr.hcl",7 Vars: map[string]string{8 "aws_region": "ap-southeast-2",9 },10 }11 amiID := packer.BuildArtifact(t, packerOptions)12 defer aws.DeleteAmiAndAllSnapshots(t, "ap-southeast-2", amiID)1314 // Step 2: Deploy infrastructure with Terraform using the AMI15 terraformOptions := &terraform.Options{16 TerraformDir: "../terraform",17 Vars: map[string]interface{}{18 "ami_id": amiID,19 },20 }21 defer terraform.Destroy(t, terraformOptions)22 terraform.InitAndApply(t, terraformOptions)2324 // Step 3: Deploy app with Kubernetes25 k8sOptions := k8s.NewKubectlOptions("", "", "default")26 k8s.KubectlApply(t, k8sOptions, "../k8s/deployment.yaml")27 defer k8s.KubectlDelete(t, k8sOptions, "../k8s/deployment.yaml")2829 // Step 4: Validate everything works together30 url := terraform.Output(t, terraformOptions, "load_balancer_url")31 http_helper.HttpGetWithRetry(t, url, nil, 200, "", 30, 10*time.Second)32}Output Validation: The Most Common Use Case
If you just want to test your module outputs are correct, which should you use?
The answer is clear: terraform test.
Why terraform test Wins for Output Testing
- Zero dependencies — No Go installation required
- Same language — Tests in HCL, matching your modules
- Fast execution — Plan-only tests run in 5-15 seconds
- No cloud costs — Mock providers since v1.7
- Native tooling — Just run
terraform test
1# tests/outputs.tftest.hcl2variables {3 project_name = "myproject"4 environment = "test"5}67run "verify_output_formats" {8 command = plan # No infrastructure created910 # Validate naming convention11 assert {12 condition = can(regex("^myproject-test-", output.resource_name))13 error_message = "Resource name should follow naming convention"14 }1516 # Validate output is a list17 assert {18 condition = can(tolist(output.availability_zones))19 error_message = "availability_zones should be a list"20 }2122 # Validate minimum count23 assert {24 condition = length(output.subnet_ids) >= 225 error_message = "Should have at least 2 subnets"26 }27}When Terratest is Still Better for Outputs
Use Terratest when you need to validate outputs against real infrastructure behavior:
1func TestOutputsAgainstRealInfra(t *testing.T) {2 terraform.InitAndApply(t, terraformOptions)34 // Get output and validate against actual AWS state5 bucketName := terraform.Output(t, terraformOptions, "bucket_name")67 // Verify the bucket actually exists and has correct config8 aws.AssertS3BucketExists(t, "ap-southeast-2", bucketName)9 versioning := aws.GetS3BucketVersioning(t, "ap-southeast-2", bucketName)10 assert.Equal(t, "Enabled", versioning.Status)11}Test Type Capabilities
Not all tests are equal. Here's what each framework can actually do:
Head-to-Head Comparison
Quick Reference Table
| Feature | Terraform Test | Terratest | Winner |
|---|---|---|---|
| Language | HCL | Go | Depends on team |
| Learning Curve | Low | High | terraform test |
| Mock Support | Yes (v1.7+) | No | terraform test |
| Real Infrastructure | Optional | Always | terraform test |
| Unit Test Speed | 5-15 seconds | N/A | terraform test |
| Integration Speed | Minutes | Minutes | Tie |
| Cloud Costs | Optional | Yes | terraform test |
| HTTP Testing | No | Yes | Terratest |
| SSH Testing | No | Yes | Terratest |
| Database Testing | No | Yes | Terratest |
| API Validation | No | Yes | Terratest |
| Multi-Tool | Terraform only | Packer, K8s, Helm, Docker | Terratest |
| Parallel Tests | v1.12+ | Native Go | Tie |
| JUnit Output | Limited | Built-in | Terratest |
| HCP Terraform | Native | Via CLI | terraform test |
| OpenTofu Support | Yes | Yes | Tie |
Speed Comparison
| Test Type | Terraform Test | Terratest |
|---|---|---|
| Syntax validation | ~2 seconds | N/A |
| Unit test (mocked) | 5-15 seconds | N/A |
| Simple integration | 2-5 minutes | 5-10 minutes |
| Complex integration | 10-20 minutes | 15-30 minutes |
| Full stack E2E | Limited | 30-60+ minutes |
The Adaptability Question
Terraform Test adapts well to:
- Module development workflows
- CI/CD pipelines needing fast feedback
- Teams without Go expertise
- Cost-conscious testing strategies
- HCP Terraform environments
Terratest adapts well to:
- Complex multi-cloud environments
- Organizations with Go expertise
- Pipelines spanning multiple tools
- Scenarios requiring real API validation
- Existing Terratest investments
When to Use Each: Decision Framework
Choose Terraform Test When:
- Team is HCL-proficient but not Go-proficient
- You need fast unit tests during development
- Testing module input validation and outputs
- Want to minimize cloud costs for testing
- Using HCP Terraform for module management
- Starting fresh without existing test infrastructure
Choose Terratest When:
- Team has strong Go expertise
- Need to validate actual infrastructure behavior
- Testing HTTP endpoints, SSH access, or API responses
- Multi-tool pipelines (Terraform + Packer + Kubernetes)
- Existing investment in Terratest infrastructure
- Complex orchestration across multiple modules
The Hybrid Approach (Recommended for 2026)
Most mature teams use both:
Layer 1 - Static Analysis (Every Commit):
1terraform fmt -check2terraform validateLayer 2 - Unit Tests (Every PR):
1# Fast mocked tests2mock_provider "aws" {}3run "unit_test" { command = plan }Layer 3 - Integration Tests (Daily/Merge to Main):
1# Real infrastructure tests2run "integration_test" { command = apply }Layer 4 - E2E Tests (Weekly/Pre-Release):
1// Full stack validation with Terratest2func TestFullStack(t *testing.T) { ... }CI/CD Integration
Terraform Test in GitHub Actions
1name: Terraform Module Tests2on:3 pull_request:4 paths:5 - 'modules/**'6 - 'tests/**'78jobs:9 unit-tests:10 runs-on: ubuntu-latest11 steps:12 - uses: actions/checkout@v41314 - name: Setup Terraform15 uses: hashicorp/setup-terraform@v316 with:17 terraform_version: 1.14.01819 - name: Terraform Init20 run: terraform init2122 - name: Run Unit Tests (Mocked)23 run: terraform test -filter=tests/unit/2425 integration-tests:26 runs-on: ubuntu-latest27 needs: unit-tests # Only run if unit tests pass28 steps:29 - uses: actions/checkout@v43031 - name: Setup Terraform32 uses: hashicorp/setup-terraform@v33334 - name: Run Integration Tests35 run: terraform test -filter=tests/integration/36 env:37 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}38 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Terratest in GitHub Actions
1name: Terratest Integration2on:3 push:4 branches: [main]5 schedule:6 - cron: '0 6 * * *' # Daily at 6 AM78jobs:9 terratest:10 runs-on: ubuntu-latest11 steps:12 - uses: actions/checkout@v41314 - name: Setup Go15 uses: actions/setup-go@v516 with:17 go-version: '1.22'1819 - name: Setup Terraform20 uses: hashicorp/setup-terraform@v321 with:22 terraform_wrapper: false # Required for Terratest2324 - name: Run Tests25 run: |26 cd test27 go test -v -timeout 60m ./...28 env:29 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}30 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Complete CI/CD Testing Pipeline
Here's how a mature IaC testing pipeline should flow:
Pipeline Configuration Example
1# .github/workflows/terraform-testing.yml2name: Complete IaC Testing Pipeline34on:5 push:6 branches: [main, develop]7 pull_request:8 branches: [main]910jobs:11 # Stage 1: Static Analysis (Every Commit)12 static-analysis:13 runs-on: ubuntu-latest14 steps:15 - uses: actions/checkout@v416 - uses: hashicorp/setup-terraform@v31718 - name: Format Check19 run: terraform fmt -check -recursive2021 - name: Validate22 run: |23 terraform init -backend=false24 terraform validate2526 - name: TFLint27 uses: terraform-linters/setup-tflint@v428 with:29 tflint_version: latest3031 - run: tflint --recursive3233 # Stage 2: Security Scanning34 security-scan:35 needs: static-analysis36 runs-on: ubuntu-latest37 steps:38 - uses: actions/checkout@v43940 - name: Checkov Scan41 uses: bridgecrewio/checkov-action@v1242 with:43 directory: .44 framework: terraform45 output_format: sarif4647 - name: tfsec48 uses: aquasecurity/tfsec-action@v1.0.04950 # Stage 3: Unit Tests (Mocked)51 unit-tests:52 needs: static-analysis53 runs-on: ubuntu-latest54 steps:55 - uses: actions/checkout@v456 - uses: hashicorp/setup-terraform@v35758 - name: Run Mocked Unit Tests59 run: terraform test -filter=tests/unit/6061 # Stage 4: Integration Tests (Real Infrastructure)62 integration-tests:63 needs: [unit-tests, security-scan]64 if: github.ref == 'refs/heads/main'65 runs-on: ubuntu-latest66 environment: testing67 steps:68 - uses: actions/checkout@v469 - uses: hashicorp/setup-terraform@v37071 - name: Integration Tests72 run: terraform test -filter=tests/integration/73 env:74 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}75 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}7677 # Stage 5: Terratest E2E (Pre-Release)78 e2e-tests:79 needs: integration-tests80 if: startsWith(github.ref, 'refs/tags/')81 runs-on: ubuntu-latest82 steps:83 - uses: actions/checkout@v484 - uses: actions/setup-go@v585 with:86 go-version: '1.22'87 - uses: hashicorp/setup-terraform@v388 with:89 terraform_wrapper: false9091 - name: Terratest Suite92 run: |93 cd test94 go test -v -timeout 90m ./...OpenTofu Testing Support
OpenTofu, the open-source Terraform fork, has full support for both testing approaches as of 2026.
OpenTofu Test Compatibility
| Feature | Terraform Test | OpenTofu Test |
|---|---|---|
.tftest.hcl syntax | Yes | Yes |
| Mock providers | Yes (v1.7+) | Yes (v1.7+) |
| Override blocks | Yes | Yes |
| Variables in tests | Yes | Yes |
| Run block commands | plan/apply | plan/apply |
Switching to OpenTofu for Tests
1# Install OpenTofu2brew install opentofu34# Run tests with OpenTofu5tofu test67# Terratest with OpenTofu8export TERRATEST_TERRAFORM_BINARY="tofu"9go test -v ./...Terratest supports OpenTofu via the TERRATEST_TERRAFORM_BINARY environment variable, making migration straightforward.
Security Testing Tools
Testing infrastructure isn't just about functionality—security validation is critical.
Security Tools Comparison
| Tool | Type | Focus | Integration | Best For |
|---|---|---|---|---|
| Checkov | Static | Security + Compliance | CI/CD, IDE | Comprehensive scanning |
| tfsec | Static | Security misconfigs | GitHub Actions | Fast PR feedback |
| Terrascan | Static | Policy as Code | Multi-cloud | OPA policies |
| TFLint | Linter | Best practices | Pre-commit | Code quality |
| Sentinel | Policy | Governance | HCP Terraform | Enterprise |
| OPA/Conftest | Policy | Custom policies | Any CI | Flexibility |
Integrating Security Testing
1# tests/security.tftest.hcl23run "verify_encryption_enabled" {4 command = plan56 assert {7 condition = alltrue([8 for bucket in aws_s3_bucket.all :9 bucket.server_side_encryption_configuration != null10 ])11 error_message = "All S3 buckets must have encryption enabled"12 }13}1415run "verify_public_access_blocked" {16 command = plan1718 assert {19 condition = alltrue([20 for bucket in aws_s3_bucket_public_access_block.all :21 bucket.block_public_acls == true &&22 bucket.block_public_policy == true23 ])24 error_message = "Public access must be blocked on all buckets"25 }26}2728run "verify_no_hardcoded_secrets" {29 command = plan3031 # Negative test - should fail if secrets detected32 expect_failures = []3334 assert {35 condition = !can(regex("AKIA[A-Z0-9]{16}", jsonencode(local.all_vars)))36 error_message = "Potential AWS access key detected in variables"37 }38}Checkov Example
1# Run Checkov against your Terraform2checkov -d . --framework terraform34# Output example5Passed checks: 42, Failed checks: 3, Skipped checks: 067Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"8 FAILED for resource: aws_s3_bucket.data9 File: /modules/s3/main.tf:1-151011Check: CKV_AWS_145: "Ensure S3 bucket encryption is enabled"12 PASSED for resource: aws_s3_bucket.dataSecurity in Your Testing Pipeline
Run security scans early—failed security checks should block PRs before expensive integration tests run.
What's New in 2026
Terraform Test Enhancements (v1.14-1.15)
| Feature | Version | Description |
|---|---|---|
| Backend in run blocks | 1.15 (alpha) | Persist state between test operations |
skip_cleanup | 1.15 | Preserve test state for debugging |
terraform test cleanup | 1.15 | Clean up orphaned test state |
| Ephemeral outputs | 1.14 | Use ephemeral values in tests |
| Enhanced JUnit | 1.14 | Better CI/CD integration |
Terratest v0.54.0
- 421 total releases
- 200+ contributors
- Enhanced retry mechanisms
- Expanded cloud provider helpers
- Active maintenance cycle
The Verdict
Terraform Test has matured into a capable native testing framework. If you're starting fresh or your team doesn't know Go, it's the obvious choice for unit testing and basic integration testing.
Terratest remains essential for complex scenarios: multi-tool pipelines, HTTP/SSH validation, and organizations with Go expertise who need maximum control.
The winning strategy in 2026: Use both. Fast mocked unit tests with terraform test during development, comprehensive integration tests with Terratest before production.
Brisbane Infrastructure Testing
At Buun Group, we help Queensland teams implement robust infrastructure testing strategies:
- Test Framework Selection — evaluate terraform test vs Terratest for your team
- CI/CD Pipeline Design — integrate testing into your deployment workflow
- Mock Strategy Development — design effective mock providers for fast feedback
- Terratest Implementation — build comprehensive Go-based test suites
We've implemented both frameworks in production. We know when each makes sense.
Need infrastructure testing guidance?
Topics
Comments
Sign in to join the conversation
LoginNo comments yet. Be the first to share your thoughts!
Found an issue with this article?
