Test Fixtures & Helpers
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.
Previous: ← Mocking & Test Doubles
Last updated: July 3, 2026