Comprehensive test-driven development for Terraform providers with iterative co-development of tests, generators, and schemas...
Comprehensive test-driven development for Terraform providers emphasizing iterative co-development of tests, generators, and schemas. This skill drives the development of production-ready provider code through systematic testing and root cause analysis.
Tests, generators, and schemas are interdependent systems that evolve together. When tests fail, the response is ALWAYS to fix the root cause - never to skip, disable, or work around.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CO-DEVELOPMENT TRIANGLE β
β β
β βββββββββββ β
β β TESTS β β
β ββββββ¬βββββ β
β β β
β βββββββββββββββββββΌββββββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β βGENERATORSβββββββΊβ SCHEMAS βββββββΊβ RESOURCESβ β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β
β Tests drive β Generator fixes β Schema corrections β Resource updates β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ALWAYS develop and fix unless ONE of these conditions applies:
| Skip Condition | Example | Action |
|---|---|---|
| External cloud credentials required | AWS VPC Site needs AWS_ACCESS_KEY_ID |
t.Skip("requires AWS credentials") |
| Premium/enterprise licensing required | Bot defense needs advanced license | t.Skip("requires premium licensing") |
| Third-party service account needed | External OIDC provider | t.Skip("requires external service account") |
NEVER skip for these reasons - fix them instead:
| Invalid Skip Reason | Correct Action |
|---|---|
| "Schema attribute missing" | Fix generator, regenerate |
| "State drift on nested blocks" | Fix generator's Read/Schema handling |
| "Import produces diff" | Fix ImportState function in generator |
| "API returns unexpected format" | Fix client types or resource parsing |
| "Test is too complex" | Break into smaller tests, still implement |
| "Generator doesn't support this" | Enhance generator to support it |
When a test fails, investigate and fix in this order:
1. GENERATOR ISSUE?
β Symptoms: Same failure across ALL resources of similar type
β Fix: tools/generate-all-schemas.go β go generate ./...
β
2. SCHEMA ISSUE?
β Symptoms: Attribute handling, type mismatches, nested block problems
β Fix: Generator's schema generation logic β regenerate
β
3. RESOURCE IMPLEMENTATION ISSUE?
β Symptoms: Single resource fails, API-specific parsing
β Fix: The specific resource's CRUD methods
β
4. TEST ISSUE?
β Symptoms: Wrong assertions, incorrect config, test logic error
β Fix: Test code only AFTER ruling out 1-3
CRITICAL: All Go source code MUST use tabs for indentation - this is enforced by gofmt.
| File Type | Indentation | Standard |
|---|---|---|
Go source files (.go) |
Tabs | gofmt enforced |
| Embedded Terraform/HCL in heredocs | 2 spaces | Terraform convention |
Why tabs for Go:
gofmt is the canonical Go formatter and uses tabsgofmt before commitgofmt output// β
CORRECT: Tabs for Go code indentation (gofmt standard)
func TestAccExampleResource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccExampleConfig_basic(rName),
},
},
})
}
Terraform/HCL inside Go heredocs uses 2 spaces (Terraform convention):
// β
CORRECT: Tabs for Go, 2 spaces for embedded HCL
func testAccExampleConfig_basic(rName string) string {
return fmt.Sprintf(`
resource "f5xc_namespace" "test" {
name = %[1]q
}
resource "f5xc_example" "test" {
name = %[1]q
namespace = f5xc_namespace.test.name
nested_block {
attribute = "value"
}
}
`, rName)
}
Key distinction:
func and return fmt.Sprintf lines use tabs (Go code)Always run gofmt before committing:
# Format all Go files in place
gofmt -w .
# Check formatting without modifying (CI mode)
gofmt -d . | grep -q . && echo "Formatting needed" || echo "OK"
# Format with goimports (also organizes imports)
goimports -w .
Configure your editor for Go's tab standard:
VS Code settings:
{
"[go]": {
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.formatOnSave": true
}
}
Vim settings:
" For Go files - use tabs (gofmt standard)
autocmd FileType go setlocal noexpandtab tabstop=4 shiftwidth=4
Every test development follows this iterative pattern:
# Phase 1: Write Initial Test
vim internal/provider/example_resource_test.go
# Phase 2: Run Test (expect failures)
F5XC_API_URL="https://tenant.console.ves.volterra.io" \
F5XC_P12_FILE="/path/to/cert.p12" \
F5XC_P12_PASSWORD="password" \ # pragma: allowlist secret
TF_ACC=1 go test -v -timeout 15m \
-run TestAccExampleResource_basic ./internal/provider/...
# Phase 3: Analyze Failure - Determine Root Cause
# Ask: Is this a GENERATOR issue affecting all resources?
# Is this a SCHEMA issue with attribute handling?
# Is this a RESOURCE issue with this specific API?
# Is this a TEST issue with my assertions?
# Phase 4: Fix at the Appropriate Level
# If generator: vim tools/generate-all-schemas.go && go generate ./...
# If schema: Fix in generator, regenerate
# If resource: Fix specific resource implementation
# If test: Fix test assertions/config
# Phase 5: Re-run and Verify
TF_ACC=1 go test -v -timeout 15m \
-run TestAccExampleResource_basic ./internal/provider/...
# REPEAT until test passes - NEVER skip
Test Failed
β
βββ Error mentions "attribute not found in schema"?
β βββ FIX: Generator schema generation β regenerate ALL resources
β
βββ Error shows state drift on nested blocks?
β βββ FIX: Generator's Read method handling of nested structures
β
βββ ImportStateVerify shows diff?
β βββ FIX: Generator's ImportState function
β
βββ API returns 400/422 with field error?
β βββ FIX: Generator's Create/Update request building
β
βββ Computed field not populated after Read?
β βββ FIX: Generator's Read response β state mapping
β
βββ Same error pattern across multiple resource types?
β βββ FIX: Generator (systemic issue) β regenerate
β
βββ Error only in this specific resource?
β βββ FIX: Resource implementation OR client types
β
βββ Assertion doesn't match expected value?
βββ INVESTIGATE: Is expected value correct? Is resource behavior correct?
βββ Resource behavior wrong β Fix resource
βββ Assertion wrong β Fix test
Example 1: Nested Block State Drift
TEST FAILURE: inconsistent result after apply
- default_route_pools.0.pool.namespace: "" => "test-ns"
ANALYSIS: Generator's Read method doesn't populate nested blocks correctly
FIX LOCATION: tools/generate-all-schemas.go
FIX TYPE: Update nested block flattening logic
STEPS:
1. Identify pattern in generator for nested block handling
2. Fix the flattening/expansion logic
3. go generate ./...
4. Re-run test
5. Verify ALL similar resources now work
Example 2: Missing Schema Attribute
TEST FAILURE: attribute "labels" not found in schema
ANALYSIS: Generator doesn't include labels attribute from OpenAPI spec
FIX LOCATION: tools/generate-all-schemas.go
FIX TYPE: Add labels field to schema generation
STEPS:
1. Check OpenAPI spec confirms labels exists
2. Update generator to include labels in schema
3. go generate ./...
4. Re-run test
5. Verify all resources with labels now have the attribute
Example 3: Import State Incomplete
TEST FAILURE: ImportStateVerify found differences:
- description: "test" => ""
ANALYSIS: ImportState doesn't set all attributes from API response
FIX LOCATION: Generator's ImportState template or Read method
FIX TYPE: Ensure Read populates all importable attributes
STEPS:
1. Check API response includes description
2. Fix generator's Read to map description to state
3. go generate ./...
4. Re-run import test
5. Verify import now preserves all attributes
func TestAccExampleResource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckExampleDestroy,
Steps: []resource.TestStep{
// Step 1: Create and verify
{
Config: testAccExampleConfig_basic(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName,
plancheck.ResourceActionCreate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName,
tfjsonpath.New("name"), knownvalue.StringExact(rName)),
},
},
// Step 2: Import and verify state roundtrip
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
Every testable resource MUST have:
| Test Type | Purpose | Skip Only If |
|---|---|---|
_basic |
Create, Read, basic attributes | External credentials/licensing |
_update |
Modify mutable attributes | External credentials/licensing |
_import |
Import existing resource | External credentials/licensing |
_disappears |
Handle external deletion | External credentials/licensing |
| Data source test | Verify data source reads resource | External credentials/licensing |
// REQUIRED test structure for each resource
func TestAccExampleResource_basic(t *testing.T) { ... }
func TestAccExampleResource_update(t *testing.T) { ... }
func TestAccExampleResource_disappears(t *testing.T) { ... }
func TestAccExampleDataSource_basic(t *testing.T) { ... }
Validate Terraform plan before resources are applied:
{
Config: testAccConfig,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
// Verify expected action
plancheck.ExpectResourceAction("f5xc_namespace.test",
plancheck.ResourceActionCreate),
// Verify known values in plan
plancheck.ExpectKnownValue("f5xc_namespace.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test-ns")),
// Verify unknown values (computed)
plancheck.ExpectUnknownValue("f5xc_namespace.test",
tfjsonpath.New("id")),
},
PostApply: []plancheck.PlanCheck{
// After apply, plan should be empty (no drift)
plancheck.ExpectEmptyPlan(),
},
},
}
Validate Terraform state after resources are applied:
{
Config: testAccConfig,
ConfigStateChecks: []statecheck.StateCheck{
// Verify exact string value
statecheck.ExpectKnownValue("f5xc_namespace.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test-ns")),
// Verify boolean
statecheck.ExpectKnownValue("f5xc_app_firewall.test",
tfjsonpath.New("blocking"),
knownvalue.Bool(true)),
// Verify list size
statecheck.ExpectKnownValue("f5xc_http_lb.test",
tfjsonpath.New("domains"),
knownvalue.ListSizeExact(2)),
// Verify nested object
statecheck.ExpectKnownValue("f5xc_http_lb.test",
tfjsonpath.New("default_route_pools").AtSliceIndex(0),
knownvalue.ObjectPartial(map[string]knownvalue.Check{
"weight": knownvalue.Int64Exact(1),
"priority": knownvalue.Int64Exact(1),
})),
},
}
| Check Type | Usage | Example |
|---|---|---|
StringExact |
Exact string match | knownvalue.StringExact("value") |
StringRegexp |
Regex match | knownvalue.StringRegexp(regexp.MustCompile(^tf-)) |
Bool |
Boolean value | knownvalue.Bool(true) |
Int64Exact |
Exact integer | knownvalue.Int64Exact(42) |
ListExact |
Exact list | knownvalue.ListExact([]knownvalue.Check{...}) |
ListSizeExact |
List length | knownvalue.ListSizeExact(3) |
ListPartial |
Partial list match | knownvalue.ListPartial(map[int]knownvalue.Check{0: ...}) |
ObjectExact |
Exact object | knownvalue.ObjectExact(map[string]knownvalue.Check{...}) |
ObjectPartial |
Partial object | knownvalue.ObjectPartial(map[string]knownvalue.Check{...}) |
Null |
Null value | knownvalue.Null() |
NotNull |
Not null | knownvalue.NotNull() |
// Simple attribute
tfjsonpath.New("name")
// Nested attribute
tfjsonpath.New("metadata").AtMapKey("labels")
// List index
tfjsonpath.New("domains").AtSliceIndex(0)
// Complex nested path
tfjsonpath.New("spec").AtMapKey("routes").AtSliceIndex(0).AtMapKey("match")
RULE: Create a custom test namespace unless the API spec EXPLICITLY requires system, default, or shared.
// β
CORRECT: Custom namespace
func testAccExampleConfig_basic(rName string) string {
return fmt.Sprintf(`
resource "f5xc_namespace" "test" {
name = %[1]q
}
resource "f5xc_example" "test" {
name = %[1]q
namespace = f5xc_namespace.test.name
}
`, rName)
}
// β WRONG: Using system namespace without spec requirement
func testAccExampleConfig_wrong(name string) string {
return fmt.Sprintf(`
resource "f5xc_example" "test" {
name = %[1]q
namespace = "system" // NEVER unless spec requires it
}
`, name)
}
Does the OpenAPI spec require a specific namespace?
βββ YES: system/default/shared required
β βββ Document WHY in test comments with spec reference
β βββ Use the required namespace
βββ NO: Custom namespace allowed
βββ ALWAYS create f5xc_namespace.test and reference it
func TestAccExampleResource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckExampleDestroy,
Steps: []resource.TestStep{
// Create
{
Config: testAccExampleConfig_basic(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName,
plancheck.ResourceActionCreate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName,
tfjsonpath.New("name"), knownvalue.StringExact(rName)),
},
},
// Update
{
Config: testAccExampleConfig_updated(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName,
plancheck.ResourceActionUpdate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName,
tfjsonpath.New("description"),
knownvalue.StringExact("updated")),
},
},
// Import
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccCheckExampleDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*client.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "f5xc_example" {
continue
}
namespace := rs.Primary.Attributes["namespace"]
name := rs.Primary.Attributes["name"]
_, err := client.GetExample(context.Background(), namespace, name)
if err == nil {
return fmt.Errorf("f5xc_example %s/%s still exists", namespace, name)
}
if !client.IsNotFoundError(err) {
return fmt.Errorf("error checking f5xc_example %s/%s: %w",
namespace, name, err)
}
}
return nil
}
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: func(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return "", fmt.Errorf("resource not found: %s", resourceName)
}
return fmt.Sprintf("%s/%s",
rs.Primary.Attributes["namespace"],
rs.Primary.Attributes["name"]), nil
},
}
func TestAccExampleDataSource_basic(t *testing.T) {
t.Parallel()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "f5xc_example.test"
dataSourceName := "data.f5xc_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccExampleDataSourceConfig(rName),
Check: resource.ComposeAggregateTestCheckFunc(
// Compare data source to resource
resource.TestCheckResourceAttrPair(
dataSourceName, "name",
resourceName, "name"),
resource.TestCheckResourceAttrPair(
dataSourceName, "id",
resourceName, "id"),
resource.TestCheckResourceAttrPair(
dataSourceName, "namespace",
resourceName, "namespace"),
),
},
},
})
}
func testAccExampleDataSourceConfig(rName string) string {
return fmt.Sprintf(`
resource "f5xc_namespace" "test" {
name = %[1]q
}
resource "f5xc_example" "test" {
name = %[1]q
namespace = f5xc_namespace.test.name
}
data "f5xc_example" "test" {
name = f5xc_example.test.name
namespace = f5xc_example.test.namespace
}
`, rName)
}
| Failure | Root Cause | Fix Location | Fix Action |
|---|---|---|---|
attribute not found in schema |
Generator schema incomplete | Generator | Add attribute to schema generation |
planned value does not match |
Computed attribute not set | Generator Read | Map API response to state |
inconsistent result after apply |
Read not populating state | Generator Read | Fix response β state mapping |
import produces diff |
ImportState incomplete | Generator ImportState | Ensure all attributes set |
CheckDestroy failed |
Delete not working | Resource Delete | Fix delete API call |
namespace not found |
Using system namespace | Test config | Use custom namespace |
permission denied |
Wrong namespace | Test config | Check namespace permissions |
ExpectResourceAction failed |
Wrong action detected | Resource/Test | Check ForceNew attributes |
| State drift on nested blocks | Flattening logic wrong | Generator | Fix nested block handling |
| Boolean always false | Type conversion issue | Generator Schema | Fix bool type handling |
# Step 1: Enable debug logging
TF_LOG=DEBUG TF_ACC=1 go test -v -timeout 15m \
-run TestAccExampleResource_basic ./internal/provider/... 2>&1 | tee test.log
# Step 2: Find the API request/response
grep -A 20 "HTTP Request" test.log
grep -A 50 "HTTP Response" test.log
# Step 3: Compare API response to state
# Look for fields in response not making it to state
# Look for type mismatches (string vs int, etc.)
# Step 4: Identify fix location
# If API has data but state doesn't β Generator Read method
# If state has data but plan shows change β Generator Schema/Defaults
# If API returns error β Generator Create/Update request building
| Category | Examples | Action |
|---|---|---|
| Core resources | namespace, healthcheck, origin_pool | Full test suite |
| Security policies | app_firewall, service_policy, rate_limiter | Full test suite |
| Load balancers | http_loadbalancer, tcp_loadbalancer | Full test suite |
| Network config | virtual_network, network_policy | Full test suite |
| DNS resources | dns_zone, dns_domain | Full test suite |
| Configuration objects | Any policy, rule, or config resource | Full test suite |
| Category | Examples | Skip Reason |
|---|---|---|
| Cloud sites | aws_vpc_site, azure_vnet_site, gcp_vpc_site | Requires cloud credentials |
| Cloud integrations | cloud_credentials (AWS/Azure/GCP type) | Requires cloud credentials |
| Premium features | bot_defense*, advanced_* | Requires premium licensing |
| Third-party | External OIDC, external integrations | Requires external accounts |
// Proper skip documentation
func TestAccAWSVPCSite_basic(t *testing.T) {
t.Skip("Skipping: requires AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)")
}
func TestAccBotDefenseAdvanced_basic(t *testing.T) {
t.Skip("Skipping: requires F5 XC premium/enterprise licensing")
}
# Required for ALL acceptance tests
export F5XC_P12_FILE="/path/to/api-certificate.p12"
export F5XC_P12_PASSWORD="your-password" # pragma: allowlist secret
export F5XC_API_URL="https://tenant.console.ves.volterra.io"
export TF_ACC=1
# Single test
go test -v -timeout 15m -run=TestAccNamespaceResource_basic ./internal/provider/...
# All tests for a resource
go test -v -timeout 30m -run=TestAccNamespace ./internal/provider/...
# With debug logging
TF_LOG=DEBUG go test -v -timeout 15m -run=TestAccNamespaceResource_basic ./internal/provider/...
# With parallel limit
go test -v -timeout 30m -parallel=4 ./internal/provider/...
# Multiple specific tests
go test -v -timeout 30m -run="TestAccNamespaceResource_basic|TestAccHealthcheckResource_basic" ./internal/provider/...
t.Parallel()ImportStateVerifyIgnore without documented reason