Module 10: Testing Strategy
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:
testingpackage - Built-in test frameworktestify/assert- Assertions librarytestify/mock- Mocking framework
Terraform Testing:
terraform-plugin-testing- Acceptance test frameworkterraform-plugin-framework- Provider framework- Test fixtures and providers
Additional Tools:
go test- Run testsgo test -cover- Coverage reportsgo test -v- Verbose outputgo 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