Testing Resources

6 minute read

Testing Resources Overview

Resources require comprehensive testing of Create, Read, Update, Delete operations plus import, upgrades, and edge cases.

Testing Create Operation

Basic Create Test

func TestAccCacheResource_create(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_basic(rName),
                Check: resource.ComposeAggregateTestCheckFunc(
                    // Verify basic attributes
                    resource.TestCheckResourceAttr(resourceName, "name", rName),
                    resource.TestCheckResourceAttr(resourceName, "engine", "redis"),
                    resource.TestCheckResourceAttr(resourceName, "engine_version", "7.0"),
                    resource.TestCheckResourceAttr(resourceName, "node_type", "cache.t3.micro"),
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "1"),
                    
                    // Verify computed attributes are set
                    resource.TestCheckResourceAttrSet(resourceName, "id"),
                    resource.TestCheckResourceAttrSet(resourceName, "endpoint"),
                    resource.TestCheckResourceAttrSet(resourceName, "created_at"),
                    
                    // Verify status
                    resource.TestCheckResourceAttr(resourceName, "status", "available"),
                    
                    // Custom check
                    testAccCheckCacheExists(resourceName),
                ),
            },
        },
    })
}

Test with All Attributes

func TestAccCacheResource_complete(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_complete(rName),
                Check: resource.ComposeAggregateTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", rName),
                    resource.TestCheckResourceAttr(resourceName, "engine", "redis"),
                    resource.TestCheckResourceAttr(resourceName, "engine_version", "7.0"),
                    resource.TestCheckResourceAttr(resourceName, "node_type", "cache.t3.micro"),
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "2"),
                    resource.TestCheckResourceAttr(resourceName, "port", "6379"),
                    resource.TestCheckResourceAttr(resourceName, "parameter_group_name", "default.redis7"),
                    resource.TestCheckResourceAttr(resourceName, "maintenance_window", "sun:05:00-sun:06:00"),
                    resource.TestCheckResourceAttr(resourceName, "snapshot_retention_limit", "7"),
                    
                    // Tags
                    resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
                    resource.TestCheckResourceAttr(resourceName, "tags.Environment", "test"),
                    resource.TestCheckResourceAttr(resourceName, "tags.Team", "platform"),
                ),
            },
        },
    })
}

func testAccCacheResourceConfig_complete(rName 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                = 2
  port                     = 6379
  parameter_group_name     = "default.redis7"
  maintenance_window       = "sun:05:00-sun:06:00"
  snapshot_retention_limit = 7
  
  tags = {
    Environment = "test"
    Team        = "platform"
  }
}
`, rName)
}

Testing Update Operations

In-Place Update

func TestAccCacheResource_updateNumNodes(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_numNodes(rName, 1),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "1"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_numNodes(rName, 3),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "3"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_numNodes(rName, 2),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "num_nodes", "2"),
                ),
            },
        },
    })
}

Force Replacement

func TestAccCacheResource_updateEngine_forceNew(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    var cache1, cache2 client.Cache
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_engine(rName, "redis"),
                Check: resource.ComposeTestCheckFunc(
                    testAccCheckCacheExists(resourceName, &cache1),
                    resource.TestCheckResourceAttr(resourceName, "engine", "redis"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_engine(rName, "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 unchanged: %s", before.ID)
        }
        return nil
    }
}

Update Tags

func TestAccCacheResource_updateTags(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_tags(rName, map[string]string{
                    "Environment": "test",
                }),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
                    resource.TestCheckResourceAttr(resourceName, "tags.Environment", "test"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_tags(rName, map[string]string{
                    "Environment": "production",
                    "Team":        "platform",
                }),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "tags.%", "2"),
                    resource.TestCheckResourceAttr(resourceName, "tags.Environment", "production"),
                    resource.TestCheckResourceAttr(resourceName, "tags.Team", "platform"),
                ),
            },
            {
                Config: testAccCacheResourceConfig_basic(rName),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "tags.%", "0"),
                ),
            },
        },
    })
}

Testing Delete Operation

Standard Delete

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)
        }
        
        // Verify it's actually not found
        if !client.IsNotFoundError(err) {
            return fmt.Errorf("Unexpected error checking cache deletion: %w", err)
        }
    }
    
    return nil
}

Testing Import

Simple Import

func TestAccCacheResource_import(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_basic(rName),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", rName),
                ),
            },
            {
                ResourceName:      resourceName,
                ImportState:       true,
                ImportStateVerify: true,
            },
        },
    })
}

Import with Ignore Fields

func TestAccCacheResource_importWithPassword(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_withPassword(rName),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr(resourceName, "name", rName),
                    resource.TestCheckResourceAttrSet(resourceName, "password"),
                ),
            },
            {
                ResourceName:      resourceName,
                ImportState:       true,
                ImportStateVerify: true,
                // Password can't be imported from API
                ImportStateVerifyIgnore: []string{"password"},
            },
        },
    })
}

Composite ID Import

func TestAccCacheResource_importCompositeID(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_basic(rName),
            },
            {
                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 Computed Attributes

Verify Computed Values

func TestAccCacheResource_computedEndpoint(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    resourceName := "cachecloud_cache.test"
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_basic(rName),
                Check: resource.ComposeTestCheckFunc(
                    // Endpoint should be computed
                    resource.TestCheckResourceAttrSet(resourceName, "endpoint"),
                    resource.TestMatchResourceAttr(resourceName, "endpoint", 
                        regexp.MustCompile(`^[a-z0-9\-]+\.cache\.example\.com:\d+$`)),
                    
                    // Status should be computed
                    resource.TestCheckResourceAttrSet(resourceName, "status"),
                    resource.TestCheckResourceAttr(resourceName, "status", "available"),
                    
                    // Timestamps should be computed
                    resource.TestCheckResourceAttrSet(resourceName, "created_at"),
                    resource.TestCheckResourceAttrSet(resourceName, "updated_at"),
                ),
            },
        },
    })
}

Testing Error Cases

Invalid Configuration

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

func TestAccCacheResource_invalidPortRange(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config:      testAccCacheResourceConfig_port(rName, 80),
                ExpectError: regexp.MustCompile(`port must be between 1024 and 65535`),
            },
        },
    })
}

API Errors

func TestAccCacheResource_nameConflict(t *testing.T) {
    rName := acctest.RandomWithPrefix("tf-acc-test")
    
    resource.Test(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        CheckDestroy:             testAccCheckCacheDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccCacheResourceConfig_duplicate(rName),
                ExpectError: regexp.MustCompile(`cache with name .+ already exists`),
            },
        },
    })
}

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

resource "cachecloud_cache" "second" {
  name      = %[1]q  # Same name - should error
  engine    = "redis"
  node_type = "cache.t3.micro"
  num_nodes = 1
  
  depends_on = [cachecloud_cache.first]
}
`, rName)
}

Testing with Dependencies

Resource Depends On

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

func testAccCacheResourceConfig_vpc(rName string) string {
    return 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
}
`, rName)
}

Best Practices

1. Test All CRUD Operations

// ✅ Good: Complete lifecycle
func TestAccCacheResource_lifecycle(t *testing.T) {
    Steps: []resource.TestStep{
        {Config: createConfig, Check: createChecks},  // Create
        {Config: updateConfig, Check: updateChecks},  // Update
        {ImportState: true},                          // Import
        // Delete tested via CheckDestroy
    }
}

2. Use Random Names

// ✅ Good: Unique names prevent conflicts
rName := acctest.RandomWithPrefix("tf-acc-test")

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

3. Verify CheckDestroy

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

Key Takeaways

  • ✅ Test complete CRUD lifecycle
  • ✅ Verify all attributes (required, optional, computed)
  • ✅ Test updates (in-place and force-new)
  • ✅ Always test import functionality
  • ✅ Verify computed attributes are set
  • ✅ Test with resource dependencies
  • ✅ Check error scenarios
  • ✅ Use CheckDestroy for cleanup verification
  • ✅ Generate unique resource names
  • ✅ Test tags and metadata

What’s Next?

Learn how to test data sources with queries and filters.

Next: Testing Data Sources →


Previous: ← Acceptance Testing Framework

Last updated: July 3, 2026

Updated: