provider-test-patterns

Provider Acceptance Test Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "provider-test-patterns" with this command: npx skills add hashicorp/agent-skills/hashicorp-agent-skills-provider-test-patterns

Provider Acceptance Test Patterns

Patterns for writing acceptance tests using terraform-plugin-testing with the Plugin Framework.

Source: HashiCorp Testing Patterns

References (load when needed):

  • references/checks.md — statecheck, plancheck, knownvalue types, tfjsonpath, comparers

  • references/sweepers.md — sweeper setup, TestMain, dependencies

  • references/ephemeral.md — ephemeral resource testing, echoprovider, multi-step patterns

Test Lifecycle

The framework runs each TestStep through: plan → apply → refresh → final plan. If the final plan shows a diff, the test fails (unless ExpectNonEmptyPlan is set). After all steps, destroy runs followed by CheckDestroy . This means every test automatically verifies that configurations apply cleanly and produce no drift — no assertions needed for that.

Test Function Structure

func TestAccExample_basic(t *testing.T) { var widget example.Widget rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "example_widget.test"

resource.ParallelTest(t, resource.TestCase{
    PreCheck:                 func() { testAccPreCheck(t) },
    ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
    CheckDestroy:             testAccCheckExampleDestroy,
    Steps: []resource.TestStep{
        {
            Config: testAccExampleConfig_basic(rName),
            ConfigStateChecks: []statecheck.StateCheck{
                stateCheckExampleExists(resourceName, &widget),
                statecheck.ExpectKnownValue(resourceName,
                    tfjsonpath.New("name"), knownvalue.StringExact(rName)),
                statecheck.ExpectKnownValue(resourceName,
                    tfjsonpath.New("id"), knownvalue.NotNull()),
            },
        },
    },
})

}

Use resource.ParallelTest by default. Use resource.Test only when tests share state or cannot run concurrently.

Provider Factory

// provider_test.go — Plugin Framework with Protocol 6 (use Protocol5 variant if needed) var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ "example": providerserver.NewProtocol6WithError(New("test")()), }

TestCase Fields

Field Purpose

PreCheck

func() — verify prerequisites (env vars, API access)

ProtoV6ProviderFactories

Plugin Framework provider factories

CheckDestroy

TestCheckFunc — verify resources destroyed after all steps

Steps

[]TestStep — sequential test operations

TerraformVersionChecks

[]tfversion.TerraformVersionCheck — gate by CLI version

TestStep Fields

Config Mode

Field Purpose

Config

Inline HCL string to apply

ConfigStateChecks

[]statecheck.StateCheck — modern assertions (preferred)

ConfigPlanChecks

resource.ConfigPlanChecks{PreApply: []plancheck.PlanCheck{...}}

ExpectError

*regexp.Regexp — expect failure matching pattern

ExpectNonEmptyPlan

bool — expect non-empty plan after apply

PlanOnly

bool — plan without applying

Destroy

bool — run destroy step

PreConfig

func() — setup before step

Import Mode

Field Purpose

ImportState

true to enable import mode

ImportStateVerify

Verify imported state matches prior state

ImportStateVerifyIgnore

[]string — attributes to skip during verify

ImportStateKind

resource.ImportBlockWithID — import block generation

ResourceName

Resource address to import

ImportStateId

Override the ID used for import

Check Functions

Modern: ConfigStateChecks (preferred)

Type-safe with aggregated error reporting. Compose built-in checks with custom statecheck.StateCheck implementations. See references/checks.md for full knownvalue types, tfjsonpath navigation, and comparers.

ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact("my-widget")), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("enabled"), knownvalue.Bool(true)), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("id"), knownvalue.NotNull()), statecheck.ExpectSensitiveValue(resourceName, tfjsonpath.New("api_key")), },

Do not mix Check (legacy) and ConfigStateChecks in the same step.

Legacy: Check (for CheckDestroy and migration)

CheckDestroy on TestCase requires TestCheckFunc . The Check field on TestStep also accepts TestCheckFunc but prefer ConfigStateChecks for new tests.

Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(name, "key", "expected"), resource.TestCheckResourceAttrSet(name, "id"), resource.TestCheckNoResourceAttr(name, "removed"), resource.TestMatchResourceAttr(name, "url", regexp.MustCompile(^https://)), resource.TestCheckResourceAttrPair(res1, "ref_id", res2, "id"), ),

ComposeAggregateTestCheckFunc reports all errors; ComposeTestCheckFunc

fails fast on the first.

Config Helpers

Use numbered format verbs — %[1]q for quoted strings, %[1]s for raw:

func testAccExampleConfig_basic(rName string) string { return fmt.Sprintf(resource "example_widget" "test" { name = %[1]q }, rName) }

func testAccExampleConfig_full(rName, description string) string { return fmt.Sprintf(resource "example_widget" "test" { name = %[1]q description = %[2]q enabled = true }, rName, description) }

Scenario Patterns

Basic + Update (combine in one test — updates are supersets of basic)

Steps: []resource.TestStep{ { Config: testAccExampleConfig_basic(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(rName)), }, }, { Config: testAccExampleConfig_full(rName, "updated"), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("description"), knownvalue.StringExact("updated")), }, }, },

Import

After a config step, verify import produces identical state. Use ImportStateKind for import block generation:

{ ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateKind: resource.ImportBlockWithID, },

Disappears (resource deleted externally)

{ Config: testAccExampleConfig_basic(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleExists(resourceName, &widget), stateCheckExampleDisappears(resourceName), }, ExpectNonEmptyPlan: true, },

Validation (expect error)

{ Config: testAccExampleConfig_invalidName(""), ExpectError: regexp.MustCompile(name must not be empty), },

Regression (two-commit workflow)

A proper bug fix uses at least two commits: first commit the regression test (which fails, confirming the bug), then commit the fix (test passes). This lets reviewers independently verify the test reproduces the issue by checking out the first commit, then advancing to the fix.

Name and document regression tests to identify the issue they fix. Include a link to the original bug report when possible.

// TestAccExample_regressionGH1234 verifies fix for https://github.com/org/repo/issues/1234 func TestAccExample_regressionGH1234(t *testing.T) { rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "example_widget.test"

resource.ParallelTest(t, resource.TestCase{
    PreCheck:                 func() { testAccPreCheck(t) },
    ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
    CheckDestroy:             testAccCheckExampleDestroy,
    Steps: []resource.TestStep{
        {
            // Reproduce the issue: this config triggered the bug
            Config: testAccExampleConfig_regressionGH1234(rName),
            ConfigStateChecks: []statecheck.StateCheck{
                stateCheckExampleExists(resourceName, nil),
                statecheck.ExpectKnownValue(resourceName,
                    tfjsonpath.New("computed_field"), knownvalue.NotNull()),
            },
        },
    },
})

}

Helper Functions

Custom StateCheck: Exists

Implement statecheck.StateCheck for API existence verification. Separate the exists check into its own function for reuse across steps — the source recommends this as a design principle:

type exampleExistsCheck struct { resourceAddress string widget *example.Widget }

func (e exampleExistsCheck) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) { r, err := stateResourceAtAddress(req.State, e.resourceAddress) if err != nil { resp.Error = err return }

id, ok := r.AttributeValues["id"].(string)
if !ok {
    resp.Error = fmt.Errorf("no id found for %s", e.resourceAddress)
    return
}

conn := testAccAPIClient()
widget, err := conn.GetWidget(id)
if err != nil {
    resp.Error = fmt.Errorf("%s not found via API: %w", e.resourceAddress, err)
    return
}

if e.widget != nil {
    *e.widget = *widget
}

}

func stateCheckExampleExists(name string, widget *example.Widget) statecheck.StateCheck { return exampleExistsCheck{resourceAddress: name, widget: widget} }

Custom StateCheck: Disappears

Delete a resource via API to simulate external deletion:

type exampleDisappearsCheck struct { resourceAddress string }

func (e exampleDisappearsCheck) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) { r, err := stateResourceAtAddress(req.State, e.resourceAddress) if err != nil { resp.Error = err return }

id := r.AttributeValues["id"].(string)
conn := testAccAPIClient()
resp.Error = conn.DeleteWidget(id)

}

func stateCheckExampleDisappears(name string) statecheck.StateCheck { return exampleDisappearsCheck{resourceAddress: name} }

State Resource Lookup (shared utility)

func stateResourceAtAddress(state *tfjson.State, address string) (*tfjson.StateResource, error) { if state == nil || state.Values == nil || state.Values.RootModule == nil { return nil, fmt.Errorf("no state available") } for _, r := range state.Values.RootModule.Resources { if r.Address == address { return r, nil } } return nil, fmt.Errorf("not found in state: %s", address) }

Destroy Check (TestCheckFunc — required by CheckDestroy)

func testAccCheckExampleDestroy(s *terraform.State) error { conn := testAccAPIClient() for _, rs := range s.RootModule().Resources { if rs.Type != "example_widget" { continue } _, err := conn.GetWidget(rs.Primary.ID) if err == nil { return fmt.Errorf("widget %s still exists", rs.Primary.ID) } if !isNotFoundError(err) { return err } } return nil }

PreCheck

func testAccPreCheck(t *testing.T) { t.Helper() if os.Getenv("EXAMPLE_API_KEY") == "" { t.Fatal("EXAMPLE_API_KEY must be set for acceptance tests") } }

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

terraform-style-guide

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

terraform-test

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

refactor-module

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

terraform-stacks

No summary provided by upstream source.

Repository SourceNeeds Review