Module 10: Testing Strategy

5 minute read

Module Overview

Testing is critical for provider reliability. This module teaches you to write unit tests, acceptance tests, and integration tests that give you confidence in your provider’s behavior.

What You’ll Learn:

  • Unit testing fundamentals
  • Acceptance testing framework
  • Testing resources and data sources
  • Mocking API clients
  • Test fixtures and helpers
  • Coverage and best practices

Time: 4-5 hours

Why Testing Matters

Terraform providers manage critical infrastructure. Bugs can have serious consequences:

❌ Without Tests:
  • Changes break existing functionality
  • Edge cases cause panics
  • Upgrades introduce regressions
  • Users lose trust

✅ With Tests:
  • Catch bugs before release
  • Safe refactoring
  • Document expected behavior
  • Enable confident iteration

Testing Pyramid

              ┌─────────────────┐
              │   Acceptance    │  ← Test real API interactions
              │     Tests       │    (slow, expensive, complete)
              └─────────────────┘
             ┌───────────────────┐
             │  Integration Tests │ ← Test component interactions
             │   (with mocks)     │   (medium speed, comprehensive)
             └───────────────────┘
            ┌─────────────────────┐
            │     Unit Tests       │ ← Test individual functions
            │  (schema, helpers)   │   (fast, isolated, many)
            └─────────────────────┘

Test Distribution:

  • Unit Tests: 60-70% - Fast, many, isolated
  • Integration Tests: 20-30% - Medium, comprehensive
  • Acceptance Tests: 10-20% - Slow, complete, real API

Types of Tests

1. Unit Tests

Test individual functions in isolation:

func TestValidateEngineName(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {"valid redis", "redis", false},
        {"valid memcached", "memcached", false},
        {"invalid empty", "", true},
        {"invalid unknown", "postgres", true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validateEngineName(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("validateEngineName() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

2. Acceptance Tests

Test complete resource lifecycle with real API:

func TestAccCacheResource(t *testing.T) {
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig("test-cache"),
                Check: resource.ComposeAggregateTestCheckFunc(
                    resource.TestCheckResourceAttr("cachecloud_cache.test", "name", "test-cache"),
                    resource.TestCheckResourceAttr("cachecloud_cache.test", "engine", "redis"),
                    resource.TestCheckResourceAttrSet("cachecloud_cache.test", "id"),
                ),
            },
        },
    })
}

3. Integration Tests

Test with mocked APIs:

func TestCacheResource_Create(t *testing.T) {
    mockClient := &MockClient{
        CreateCacheFunc: func(ctx context.Context, input client.CreateCacheInput) (*client.Cache, error) {
            return &client.Cache{
                ID:     "cache-123",
                Name:   input.Name,
                Engine: input.Engine,
            }, nil
        },
    }
    
    resource := &CacheResource{client: mockClient}
    
    // Test Create operation...
}

Module Chapters

Chapter 1: Unit Testing Fundamentals

Learn Go testing basics, table-driven tests, and testing provider code.

Chapter 2: Acceptance Testing Framework

Use Terraform’s acceptance testing framework for end-to-end tests.

Chapter 3: Testing Resources

Test resource CRUD operations, imports, and state management.

Chapter 4: Testing Data Sources

Test data source queries and attribute handling.

Chapter 5: Mocking & Test Doubles

Create mock API clients and test doubles for isolated testing.

Chapter 6: Test Fixtures & Helpers

Build reusable test fixtures and helper functions.

Chapter 7: Testing Lab

Hands-on: Write complete test suite for the CacheCloud provider.

Chapter 8: Testing Practice

Practice writing tests with comprehensive scenarios.

Learning Path

Unit Tests → Acceptance Tests → Resources → Data Sources → Mocking → Lab
     ↓             ↓               ↓            ↓           ↓        ↓
   Basics      Framework      CRUD Tests   Query Tests   Mocks   Practice
   Table       TestCase       Import       Filters       Doubles
   Driven      Steps          Upgrade      Computed

What You’ll Build

By the end of this module, you’ll have a complete test suite:

Unit Tests:

// Schema validation
func TestCacheResourceSchema(t *testing.T) { ... }

// Helper functions
func TestParseTags(t *testing.T) { ... }

Acceptance Tests:

// Basic CRUD
func TestAccCacheResource_basic(t *testing.T) { ... }

// Updates
func TestAccCacheResource_update(t *testing.T) { ... }

// Import
func TestAccCacheResource_import(t *testing.T) { ... }

Integration Tests:

// Mock client
func TestCacheResource_CreateWithMock(t *testing.T) { ... }

Testing Tools

Go Testing:

  • testing package - Built-in test framework
  • testify/assert - Assertions library
  • testify/mock - Mocking framework

Terraform Testing:

  • terraform-plugin-testing - Acceptance test framework
  • terraform-plugin-framework - Provider framework
  • Test fixtures and providers

Additional Tools:

  • go test - Run tests
  • go test -cover - Coverage reports
  • go test -v - Verbose output
  • go test -run TestName - Run specific test

Key Concepts

Unit Test: Tests individual function in isolation
Acceptance Test: Tests complete resource lifecycle with real API
Integration Test: Tests component interactions with mocks
Test Case: Single test scenario with input and expected output
Test Step: Phase in acceptance test (create, update, destroy)
Mock: Fake implementation for testing
Fixture: Reusable test data
Coverage: Percentage of code executed by tests

Best Practices Preview

Throughout this module, you’ll learn:

  • ✅ Write table-driven tests for multiple scenarios
  • ✅ Test happy path and error cases
  • ✅ Use acceptance tests for real API interactions
  • ✅ Mock external dependencies
  • ✅ Test edge cases and boundary conditions
  • ✅ Keep tests fast and isolated
  • ✅ Use descriptive test names
  • ✅ Test one thing per test
  • ✅ Clean up resources in tests
  • ✅ Aim for 80%+ code coverage

Common Testing Patterns

Pattern 1: Table-Driven Tests

func TestValidatePort(t *testing.T) {
    tests := []struct {
        name    string
        port    int
        wantErr bool
    }{
        {"valid min", 1024, false},
        {"valid max", 65535, false},
        {"invalid low", 1023, true},
        {"invalid high", 65536, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validatePort(tt.port)
            if (err != nil) != tt.wantErr {
                t.Errorf("got error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Pattern 2: Subtests

func TestCacheResource(t *testing.T) {
    t.Run("Create", func(t *testing.T) {
        // Test create operation
    })
    
    t.Run("Read", func(t *testing.T) {
        // Test read operation
    })
    
    t.Run("Update", func(t *testing.T) {
        // Test update operation
    })
    
    t.Run("Delete", func(t *testing.T) {
        // Test delete operation
    })
}

Pattern 3: Test Fixtures

func newTestCache() *Cache {
    return &Cache{
        ID:     "cache-123",
        Name:   "test-cache",
        Engine: "redis",
    }
}

func TestGetCache(t *testing.T) {
    cache := newTestCache()
    // Use cache in test...
}

Prerequisites

Before starting this module:

  • ✅ Completed Module 9: API Client Integration
  • ✅ Familiar with Go testing package
  • ✅ Understanding of provider implementation

Go knowledge needed:

  • Testing package basics
  • Table-driven tests
  • Interfaces and mocking

Common Pitfalls to Avoid

  • ❌ Only testing happy path
  • ❌ Tests that depend on external state
  • ❌ Slow tests that take minutes to run
  • ❌ Tests that don’t clean up resources
  • ❌ Brittle tests that break on minor changes
  • ❌ Testing implementation instead of behavior
  • ❌ No tests for error cases
  • ❌ Ignoring test coverage
  • ❌ Skipping acceptance tests
  • ❌ Not using mocks for unit tests

Real-World Testing Strategy

AWS Provider Approach:

aws/
├── internal/
│   ├── service/
│   │   ├── ec2/
│   │   │   ├── instance.go
│   │   │   ├── instance_test.go        # Unit tests
│   │   │   ├── instance_acc_test.go    # Acceptance tests
│   │   │   └── instance_data_source_test.go
│   └── acctest/                         # Shared test helpers
│       ├── acctest.go
│       └── provider.go

Test Naming Convention:

  • Unit tests: function_test.go
  • Acceptance tests: TestAcc{Resource}_{scenario}
  • Integration tests: Test{Component}_{scenario}

Get Started

Ready to build a comprehensive test suite?

Start with Unit Testing Fundamentals →


Previous: ← API Client Practice

Last updated: July 3, 2026

Updated: