Test Fixtures & Helpers

6 minute read

Test Fixtures Overview

Test fixtures provide reusable test data and configuration. Helpers simplify common test operations.

Common Test Helpers

Pre-Check Helper

func testAccPreCheck(t *testing.T) {
    t.Helper()
    
    if v := os.Getenv("CACHECLOUD_API_KEY"); v == "" {
        t.Fatal("CACHECLOUD_API_KEY must be set for acceptance tests")
    }
    
    if v := os.Getenv("CACHECLOUD_ENDPOINT"); v == "" {
        t.Log("CACHECLOUD_ENDPOINT not set, using default")
    }
}

Provider Factories

var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
    "cachecloud": providerserver.NewProtocol6WithError(New("test")()),
}

// For testing with multiple providers
func testAccProtoV6ProviderFactoriesWithOtherProvider() map[string]func() (tfprotov6.ProviderServer, error) {
    factories := make(map[string]func() (tfprotov6.ProviderServer, error))
    
    // Add our provider
    for k, v := range testAccProtoV6ProviderFactories {
        factories[k] = v
    }
    
    // Add external provider (e.g., AWS)
    factories["aws"] = func() (tfprotov6.ProviderServer, error) {
        return /* AWS provider */, nil
    }
    
    return factories
}

Configuration Builders

Base Configuration

func testAccBaseConfig() string {
    return `
provider "cachecloud" {
  # Configuration from environment variables
}
`
}

Resource Configuration Builders

func testAccCacheResourceConfig(name string) string {
    return testAccBaseConfig() + fmt.Sprintf(`
resource "cachecloud_cache" "test" {
  name      = %[1]q
  engine    = "redis"
  node_type = "cache.t3.micro"
  num_nodes = 1
}
`, name)
}

func testAccCacheResourceConfig_withTags(name string, tags map[string]string) string {
    tagConfig := ""
    if len(tags) > 0 {
        tagConfig = "\n  tags = {\n"
        for k, v := range tags {
            tagConfig += fmt.Sprintf("    %s = %q\n", k, v)
        }
        tagConfig += "  }"
    }
    
    return testAccBaseConfig() + fmt.Sprintf(`
resource "cachecloud_cache" "test" {
  name      = %[1]q
  engine    = "redis"
  node_type = "cache.t3.micro"
  num_nodes = 1%[2]s
}
`, name, tagConfig)
}

func testAccCacheResourceConfig_complete(name, engine, nodeType string, numNodes int) string {
    return testAccBaseConfig() + fmt.Sprintf(`
resource "cachecloud_cache" "test" {
  name      = %[1]q
  engine    = %[2]q
  node_type = %[3]q
  num_nodes = %[4]d
}
`, name, engine, nodeType, numNodes)
}

Configuration with Dependencies

func testAccCacheResourceConfig_withVPC(name string) string {
    return testAccBaseConfig() + fmt.Sprintf(`
resource "cachecloud_vpc" "test" {
  name = "%[1]s-vpc"
  cidr = "10.0.0.0/16"
}

resource "cachecloud_subnet" "test" {
  vpc_id = cachecloud_vpc.test.id
  cidr   = "10.0.1.0/24"
}

resource "cachecloud_cache" "test" {
  name      = %[1]q
  engine    = "redis"
  node_type = "cache.t3.micro"
  num_nodes = 1
  vpc_id    = cachecloud_vpc.test.id
  subnet_id = cachecloud_subnet.test.id
}
`, name)
}

Custom Check Functions

Resource Exists

func testAccCheckCacheExists(resourceName string, cache *client.Cache) resource.TestCheckFunc {
    return func(s *terraform.State) error {
        rs, ok := s.RootModule().Resources[resourceName]
        if !ok {
            return fmt.Errorf("Not found: %s", resourceName)
        }
        
        if rs.Primary.ID == "" {
            return fmt.Errorf("No Cache ID is set")
        }
        
        conn := testAccProvider.Meta().(*client.Client)
        
        output, err := conn.GetCache(context.Background(), rs.Primary.ID)
        if err != nil {
            return err
        }
        
        *cache = *output
        
        return nil
    }
}

// Simpler version without storing cache
func testAccCheckCacheExists(resourceName string) resource.TestCheckFunc {
    return func(s *terraform.State) error {
        rs, ok := s.RootModule().Resources[resourceName]
        if !ok {
            return fmt.Errorf("Not found: %s", resourceName)
        }
        
        if rs.Primary.ID == "" {
            return fmt.Errorf("No Cache ID is set")
        }
        
        conn := testAccProvider.Meta().(*client.Client)
        
        _, err := conn.GetCache(context.Background(), rs.Primary.ID)
        return err
    }
}

Resource Destroyed

func testAccCheckCacheDestroy(s *terraform.State) error {
    conn := testAccProvider.Meta().(*client.Client)
    
    for _, rs := range s.RootModule().Resources {
        if rs.Type != "cachecloud_cache" {
            continue
        }
        
        _, err := conn.GetCache(context.Background(), rs.Primary.ID)
        
        if err == nil {
            return fmt.Errorf("Cache %s still exists", rs.Primary.ID)
        }
        
        if !client.IsNotFoundError(err) {
            return fmt.Errorf("Unexpected error: %w", err)
        }
    }
    
    return nil
}

Attribute Validation

func testAccCheckCacheAttributes(cache *client.Cache, expectedName string, expectedNodes int) resource.TestCheckFunc {
    return func(s *terraform.State) error {
        if cache.Name != expectedName {
            return fmt.Errorf("Bad name: expected %s, got %s", expectedName, cache.Name)
        }
        
        if cache.NumNodes != expectedNodes {
            return fmt.Errorf("Bad num_nodes: expected %d, got %d", expectedNodes, cache.NumNodes)
        }
        
        if cache.Status != "available" {
            return fmt.Errorf("Cache not available: %s", cache.Status)
        }
        
        return nil
    }
}

Wait for Condition

import "github.com/hashicorp/terraform-plugin-testing/helper/retry"

func testAccWaitForCacheAvailable(resourceName string, timeout time.Duration) resource.TestCheckFunc {
    return func(s *terraform.State) error {
        rs, ok := s.RootModule().Resources[resourceName]
        if !ok {
            return fmt.Errorf("Not found: %s", resourceName)
        }
        
        conn := testAccProvider.Meta().(*client.Client)
        
        return retry.RetryContext(context.Background(), timeout, func() *retry.RetryError {
            cache, err := conn.GetCache(context.Background(), rs.Primary.ID)
            if err != nil {
                return retry.NonRetryableError(err)
            }
            
            if cache.Status != "available" {
                return retry.RetryableError(fmt.Errorf("Cache status: %s", cache.Status))
            }
            
            return nil
        })
    }
}

Test Data Fixtures

Model Fixtures

func testCacheModel(name string) CacheModel {
    return CacheModel{
        Name:          types.StringValue(name),
        Engine:        types.StringValue("redis"),
        EngineVersion: types.StringValue("7.0"),
        NodeType:      types.StringValue("cache.t3.micro"),
        NumNodes:      types.Int64Value(1),
        Tags:          types.MapNull(types.StringType),
    }
}

func testCacheModelWithTags(name string, tags map[string]string) CacheModel {
    model := testCacheModel(name)
    
    tagMap := make(map[string]attr.Value)
    for k, v := range tags {
        tagMap[k] = types.StringValue(v)
    }
    
    model.Tags = types.MapValueMust(types.StringType, tagMap)
    
    return model
}

API Response Fixtures

func testCacheResponse(id, name string) *client.Cache {
    return &client.Cache{
        ID:            id,
        Name:          name,
        Engine:        "redis",
        EngineVersion: "7.0",
        NodeType:      "cache.t3.micro",
        NumNodes:      1,
        Port:          6379,
        Status:        "available",
        Endpoint:      fmt.Sprintf("%s.cache.example.com:6379", id),
        CreatedAt:     time.Now().Add(-1 * time.Hour),
        UpdatedAt:     time.Now(),
    }
}

Shared Test Package

Create Test Package

// internal/acctest/acctest.go
package acctest

import (
    "os"
    "testing"
    
    "github.com/hashicorp/terraform-plugin-framework/providerserver"
    "github.com/hashicorp/terraform-plugin-go/tfprotov6"
)

var ProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
    "cachecloud": providerserver.NewProtocol6WithError(provider.New("test")()),
}

func PreCheck(t *testing.T) {
    t.Helper()
    
    if v := os.Getenv("CACHECLOUD_API_KEY"); v == "" {
        t.Fatal("CACHECLOUD_API_KEY must be set")
    }
}

func RandomName(prefix string) string {
    return acctest.RandomWithPrefix(prefix)
}

Use Shared Package

package provider

import (
    "testing"
    
    "github.com/example/terraform-provider-cachecloud/internal/acctest"
    "github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccCacheResource_basic(t *testing.T) {
    rName := acctest.RandomName("tf-acc-test")
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { acctest.PreCheck(t) },
        ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig(rName),
                // ...
            },
        },
    })
}

Configuration Templates

Template Helper

import "text/template"

func executeTemplate(tmpl string, data interface{}) string {
    t := template.Must(template.New("test").Parse(tmpl))
    
    var buf bytes.Buffer
    if err := t.Execute(&buf, data); err != nil {
        panic(err)
    }
    
    return buf.String()
}

// Usage
const cacheConfigTemplate = `
resource "cachecloud_cache" "test" {
  name          = "{{ .Name }}"
  engine        = "{{ .Engine }}"
  engine_version = "{{ .EngineVersion }}"
  node_type     = "{{ .NodeType }}"
  num_nodes     = {{ .NumNodes }}
  
  {{- if .Tags }}
  tags = {
    {{- range $key, $value := .Tags }}
    {{ $key }} = "{{ $value }}"
    {{- end }}
  }
  {{- end }}
}
`

type CacheConfig struct {
    Name          string
    Engine        string
    EngineVersion string
    NodeType      string
    NumNodes      int
    Tags          map[string]string
}

func testAccCacheResourceConfig_fromTemplate(config CacheConfig) string {
    return executeTemplate(cacheConfigTemplate, config)
}

Test Utilities

Resource Name Generator

import "github.com/hashicorp/terraform-plugin-testing/helper/acctest"

func testAccResourceName(prefix string) string {
    return acctest.RandomWithPrefix(prefix)
}

func testAccResourceNameWithSuffix(prefix, suffix string) string {
    return fmt.Sprintf("%s-%s", acctest.RandomWithPrefix(prefix), suffix)
}

Tag Builder

func testAccTags(count int) map[string]string {
    tags := make(map[string]string)
    for i := 0; i < count; i++ {
        tags[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
    }
    return tags
}

func testAccTagsConfig(tags map[string]string) string {
    if len(tags) == 0 {
        return ""
    }
    
    config := "\n  tags = {\n"
    for k, v := range tags {
        config += fmt.Sprintf("    %s = %q\n", k, v)
    }
    config += "  }"
    
    return config
}

Best Practices

1. Use Helper Functions

// ✅ Good: Reusable helper
func testAccCacheResourceConfig(name string) string {
    return fmt.Sprintf(`
resource "cachecloud_cache" "test" {
  name = %[1]q
  engine = "redis"
}
`, name)
}

// ❌ Bad: Duplicate configuration
func TestAcc1(t *testing.T) {
    Config: `resource "cachecloud_cache" "test" { name = "test1" }`
}
func TestAcc2(t *testing.T) {
    Config: `resource "cachecloud_cache" "test" { name = "test2" }`
}

2. Mark Helpers with t.Helper()

// ✅ Good: Mark as helper
func testAccPreCheck(t *testing.T) {
    t.Helper()  // Correct line numbers in errors
    // ... checks ...
}

3. Create Shared Package

// ✅ Good: Shared utilities
import "github.com/example/provider/internal/acctest"

acctest.PreCheck(t)
acctest.ProtoV6ProviderFactories

// ❌ Bad: Duplicate in every test file
func TestAcc1(t *testing.T) {
    PreCheck: func() { /* duplicate code */ }
}

Key Takeaways

  • ✅ Create reusable configuration builders
  • ✅ Use helper functions for common operations
  • ✅ Mark helpers with t.Helper()
  • ✅ Build shared test package for utilities
  • ✅ Create fixtures for test data
  • ✅ Use templates for complex configurations
  • ✅ Write custom check functions
  • ✅ Keep test code DRY
  • ✅ Generate random names to avoid conflicts
  • ✅ Share provider factories across tests

What’s Next?

Practice building a complete test suite in a hands-on lab.

Next: Testing Lab →


Previous: ← Mocking & Test Doubles

Last updated: July 3, 2026

Updated: