Acceptance Testing Framework

6 minute read

Acceptance Testing Overview

Acceptance tests verify your provider works correctly with real Terraform operations and (optionally) real APIs. They’re slower than unit tests but provide the highest confidence.

Test Framework Setup

Import Required Packages

package provider

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

Configure Provider Factories

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

func testAccPreCheck(t *testing.T) {
    // Check required environment variables
    if v := os.Getenv("CACHECLOUD_API_KEY"); v == "" {
        t.Fatal("CACHECLOUD_API_KEY must be set for acceptance tests")
    }
}

Basic Acceptance Test

Simple Resource Test

func TestAccCacheResource_basic(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.TestCheckResourceAttr("cachecloud_cache.test", "node_type", "cache.t3.micro"),
                    resource.TestCheckResourceAttrSet("cachecloud_cache.test", "id"),
                    resource.TestCheckResourceAttrSet("cachecloud_cache.test", "endpoint"),
                ),
            },
        },
    })
}

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

Test Steps

Multiple Steps for Lifecycle

func TestAccCacheResource_lifecycle(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            // Step 1: Create
            {
                Config: testAccCacheResourceConfig_basic("test-cache"),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", "test-cache"),
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "1"),
                ),
            },
            // Step 2: Update
            {
                Config: testAccCacheResourceConfig_updated("test-cache"),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", "test-cache"),
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "2"),
                ),
            },
            // Step 3: Import
            {
                ResourceName:      resourceName,
                ImportState:       true,
                ImportStateVerify: true,
            },
        },
    })
}

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

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

Test Checks

Common Check Functions

// Check attribute equals value
resource.TestCheckResourceAttr("cachecloud_cache.test", "name", "test-cache")

// Check attribute is set (not empty)
resource.TestCheckResourceAttrSet("cachecloud_cache.test", "id")

// Check attribute matches regex
resource.TestMatchResourceAttr("cachecloud_cache.test", "endpoint", regexp.MustCompile(`^cache.*\.com:\d+$`))

// Check number of items in set/list
resource.TestCheckResourceAttr("cachecloud_cache.test", "tags.%", "2")

// Check specific map key
resource.TestCheckResourceAttr("cachecloud_cache.test", "tags.env", "production")

// Check attribute pair (one references another)
resource.TestCheckResourceAttrPair(
    "cachecloud_cache.test", "vpc_id",
    "aws_vpc.test", "id",
)

// Check TypeSet contains value
resource.TestCheckTypeSetElemAttr("cachecloud_cache.test", "security_group_ids.*", "sg-12345")

// Check nested attribute
resource.TestCheckResourceAttr("cachecloud_cache.test", "redis_config.0.maxmemory_policy", "allkeys-lru")

Custom Check Functions

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
    }
}

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 err
        }
    }
    
    return nil
}

// Usage
resource.Test(t, resource.TestCase{
    PreCheck:     func() { testAccPreCheck(t) },
    CheckDestroy: testAccCheckCacheDestroy,
    Steps: []resource.TestStep{
        {
            Config: testAccCacheResourceConfig("test"),
            Check: resource.ComposeTestCheckFunc(
                testAccCheckCacheExists("cachecloud_cache.test", &cache),
                resource.TestCheckResourceAttr("cachecloud_cache.test", "name", "test"),
            ),
        },
    },
})

Testing Import

Basic Import Test

func TestAccCacheResource_import(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig("test-cache"),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", "test-cache"),
                ),
            },
            {
                ResourceName:      resourceName,
                ImportState:       true,
                ImportStateVerify: true,
                // Ignore computed attributes that can't be imported
                ImportStateVerifyIgnore: []string{"password"},
            },
        },
    })
}

Import with ID Function

func TestAccCacheResource_importWithRegion(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig("test-cache"),
            },
            {
                ResourceName:      resourceName,
                ImportState:       true,
                ImportStateVerify: true,
                ImportStateIdFunc: testAccCacheImportStateIdFunc(resourceName),
            },
        },
    })
}

func testAccCacheImportStateIdFunc(resourceName string) resource.ImportStateIdFunc {
    return func(s *terraform.State) (string, error) {
        rs, ok := s.RootModule().Resources[resourceName]
        if !ok {
            return "", fmt.Errorf("Not found: %s", resourceName)
        }
        
        // Return composite ID: region/cache-id
        return fmt.Sprintf("us-east-1/%s", rs.Primary.ID), nil
    }
}

Testing Updates

In-Place Updates

func TestAccCacheResource_update(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_tags1("test-cache", "key1", "value1"),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
                    resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_tags2("test-cache", "key1", "value1updated", "key2", "value2"),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
                    resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"),
                    resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"),
                ),
            },
        },
    })
}

func testAccCacheResourceConfig_tags1(name, tagKey1, tagValue1 string) string {
    return fmt.Sprintf(`
resource "cachecloud_cache" "test" {
  name      = %[1]q
  engine    = "redis"
  node_type = "cache.t3.micro"
  num_nodes = 1
  
  tags = {
    %[2]q = %[3]q
  }
}
`, name, tagKey1, tagValue1)
}

Force New Resource

func TestAccCacheResource_forceNew(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    var cache1, cache2 client.Cache
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_engine("test-cache", "redis"),
                Check: resource.ComposeTestCheckFunc(
                    testAccCheckCacheExists(resourceName, &cache1),
                    resource.TestCheckResourceAttr(resourceName, "engine", "redis"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_engine("test-cache", "memcached"),
                Check: resource.ComposeTestCheckFunc(
                    testAccCheckCacheExists(resourceName, &cache2),
                    resource.TestCheckResourceAttr(resourceName, "engine", "memcached"),
                    testAccCheckCacheRecreated(&cache1, &cache2),
                ),
            },
        },
    })
}

func testAccCheckCacheRecreated(before, after *client.Cache) resource.TestCheckFunc {
    return func(s *terraform.State) error {
        if before.ID == after.ID {
            return fmt.Errorf("Expected cache to be recreated, but ID is same: %s", before.ID)
        }
        return nil
    }
}

Testing Drift Detection

Detect External Changes

func TestAccCacheResource_drift(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    var cache client.Cache
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig("test-cache"),
                Check: resource.ComposeTestCheckFunc(
                    testAccCheckCacheExists(resourceName, &cache),
                ),
            },
            {
                // Modify resource outside Terraform
                PreConfig: func() {
                    conn := testAccProvider.Meta().(*client.Client)
                    conn.UpdateCache(context.Background(), cache.ID, client.UpdateCacheInput{
                        NumNodes: ptr.Int(3),
                    })
                },
                Config: testAccCacheResourceConfig("test-cache"),
                Check: resource.ComposeTestCheckFunc(
                    // Should detect drift and update
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "1"),
                ),
            },
        },
    })
}

Testing with Dependencies

Multiple Resources

func TestAccCacheResource_withVPC(t *testing.T) {
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_vpc("test-cache"),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", "test-cache"),
                    resource.TestCheckResourceAttrPair(
                        resourceName, "vpc_id",
                        "cachecloud_vpc.test", "id",
                    ),
                ),
            },
        },
    })
}

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

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

Error Testing

Expect Error

func TestAccCacheResource_invalidEngine(t *testing.T) {
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config:      testAccCacheResourceConfig_invalidEngine("test-cache"),
                ExpectError: regexp.MustCompile(`engine must be either 'redis' or 'memcached'`),
            },
        },
    })
}

func testAccCacheResourceConfig_invalidEngine(name string) string {
    return fmt.Sprintf(`
resource "cachecloud_cache" "test" {
  name      = %[1]q
  engine    = "postgres"  # Invalid
  node_type = "cache.t3.micro"
  num_nodes = 1
}
`, name)
}

Test Helpers

Random Names

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

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

Retry Logic

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

func testAccCheckCacheReady(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)
        }
        
        conn := testAccProvider.Meta().(*client.Client)
        
        return retry.RetryContext(context.Background(), 5*time.Minute, 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 not ready: %s", cache.Status))
            }
            
            return nil
        })
    }
}

Best Practices

1. Use Unique Names

// ✅ Good: Random name to avoid conflicts
rName := acctest.RandomWithPrefix("tf-acc-test")

// ❌ Bad: Fixed name can conflict
name := "test-cache"

2. Clean Up Resources

// ✅ Good: CheckDestroy verifies cleanup
resource.Test(t, resource.TestCase{
    CheckDestroy: testAccCheckCacheDestroy,
    Steps: []resource.TestStep{...},
})

// ❌ Bad: No cleanup verification
resource.Test(t, resource.TestCase{
    Steps: []resource.TestStep{...},
})

3. Test Complete Lifecycle

// ✅ Good: Create, update, import
Steps: []resource.TestStep{
    {Config: createConfig, Check: createChecks},
    {Config: updateConfig, Check: updateChecks},
    {ImportState: true, ImportStateVerify: true},
}

Key Takeaways

  • ✅ Use acceptance tests for end-to-end validation
  • ✅ Test create, read, update, delete operations
  • ✅ Verify import functionality
  • ✅ Test updates (in-place and force-new)
  • ✅ Use CheckDestroy to verify cleanup
  • ✅ Generate unique resource names
  • ✅ Test with resource dependencies
  • ✅ Verify drift detection
  • ✅ Test error scenarios
  • ✅ Use PreConfig for external changes

What’s Next?

Learn specific patterns for testing resource CRUD operations.

Next: Testing Resources →


Previous: ← Unit Testing Fundamentals

Last updated: July 3, 2026

Updated: