Acceptance Testing Framework
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.
Previous: ← Unit Testing Fundamentals
Last updated: July 3, 2026