MCPInstall

provider-test-patterns

Terraform provider acceptance test patterns using terraform-plugin-testing with the Plugin Framework. Covers test structure, TestCase/TestStep fields, ConfigStateChecks with custom statecheck.StateCheck implementations, plan checks, CompareValue for cross-step assertions, config helpers, import testing with ImportStateKind, sweepers, and scenario patterns (basic, update, disappears, validation, regression), and ephemeral resource testing with the echoprovider package. Use when writing, reviewing, or debugging provider acceptance tests, including questions about statecheck, plancheck, TestCheckFunc, CheckDestroy, ExpectError, import state verification, ephemeral resources, or how to structure test files.

Installs
3.3K
Last 24h
+17
Trending rank
#6438
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/hashicorp/agent-skills --skill provider-test-patterns
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads provider-test-patterns automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

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

FieldPurpose
PreCheckfunc() — verify prerequisites (env vars, API access)
ProtoV6ProviderFactoriesPlugin Framework provider factories
CheckDestroyTestCheckFunc — verify resources destroyed after all steps
Steps[]TestStep — sequential test operations
TerraformVersionChecks[]tfversion.TerraformVersionCheck — gate by CLI version

TestStep Fields

Config Mode

FieldPurpose
ConfigInline HCL string to apply
ConfigStateChecks[]statecheck.StateCheck — modern assertions (preferred)
ConfigPlanChecksresource.ConfigPlanChecks{PreApply: []plancheck.PlanCheck{...}}
ExpectError*regexp.Regexp — expect failure matching pattern
ExpectNonEmptyPlanbool — expect non-empty plan after apply
PlanOnlybool — plan without applying
Destroybool — run destroy step
PreConfigfunc() — setup before step

Import Mode

FieldPurpose
ImportStatetrue to enable import mode
ImportStateVerifyVerify imported state matches prior state
ImportStateVerifyIgnore[]string — attributes to skip during verify
ImportStateKindresource.ImportBlockWithID — import block generation
ResourceNameResource address to import
ImportStateIdOverride 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")
    }
}

FAQ

What does the provider-test-patterns skill do?

Terraform provider acceptance test patterns using terraform-plugin-testing with the Plugin Framework. Covers test structure, TestCase/TestStep fields, ConfigStateChecks with custom statecheck.StateCheck implementations, plan checks, CompareValue for cross-step assertions, config helpers, import testing with ImportStateKind, sweepers, and scenario patterns (basic, update, disappears, validation, regression), and ephemeral resource testing with the echoprovider package. Use when writing, reviewing, or debugging provider acceptance tests, including questions about statecheck, plancheck, TestCheckFunc, CheckDestroy, ExpectError, import state verification, ephemeral resources, or how to structure test files.

How do I install provider-test-patterns?

Run npx skills add https://github.com/hashicorp/agent-skills --skill provider-test-patterns in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/provider-test-patterns/ (personal) or .claude/skills/provider-test-patterns/ (project).

Which AI agents can use provider-test-patterns?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is provider-test-patterns safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains provider-test-patterns?

It is published by hashicorp in hashicorp/agent-skills, and it is an official skill from the maker of the technology it covers. It has 3,285 recorded installs.

More from hashicorp/agent-skills

View all →

terraform-style-guide

hashicorp/agent-skills

Generate Terraform HCL code following HashiCorp's official style conventions and best practices. Use when writing, reviewing, or generating Terraform configurations.

OfficialDevOps & Cloud12.6K installs

terraform-test

hashicorp/agent-skills

Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating infrastructure behavior with assertions, mocking providers and data sources, testing module outputs and resource configurations, or troubleshooting Terraform test syntax and execution.

OfficialTesting & QA8.8K installs

refactor-module

hashicorp/agent-skills

Transform monolithic Terraform configurations into reusable, maintainable modules following HashiCorp's module design principles and community best practices.

OfficialCode Quality & Workflow6.4K installs

terraform-stacks

hashicorp/agent-skills

Comprehensive guide for working with HashiCorp Terraform Stacks. Use when creating, modifying, or validating Terraform Stack configurations (.tfcomponent.hcl, .tfdeploy.hcl files), working with stack components and deployments from local modules, public registry, or private registry sources, managing multi-region or multi-environment infrastructure, or troubleshooting Terraform Stacks syntax and structure.

OfficialDevOps & Cloud5.7K installs

terraform-search-import

hashicorp/agent-skills

Discover existing cloud resources using Terraform Search queries and bulk import them into Terraform management. Use when bringing unmanaged infrastructure under Terraform control, auditing cloud resources, or migrating to IaC.

OfficialDevOps & Cloud4.9K installs

provider-resources

hashicorp/agent-skills

Implement Terraform Provider resources and data sources using the Plugin Framework: CRUD operations, schema design, plan modifiers and validators, not-found handling, waiters for eventually consistent APIs, import support, resource design principles, and required acceptance test coverage. Use when adding or changing a resource or data source, deciding whether an API concept should be a resource, wiring a resource to the provider's configured client, handling drift or resource-not-found, or reviewing a resource implementation before submission.

OfficialTesting & QA4.1K installs

Popular testing & qa skills

View category →

tdd

mattpocock/skills

Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.

Testing & QA1.2M installs

grilling

mattpocock/skills

Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.

Testing & QA996.2K installs

migrate-to-shoehorn

mattpocock/skills

Migrate test files from as type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace as in tests, or needs partial test data.

Testing & QA484.1K installs

run-train

lllllllama/rigorpilot-skills

Rigor Train skill for deep learning research repositories. Use when a documented or selected training command should be run conservatively for startup verification, short-run verification, full kickoff, or resume, with command, config, seed, log, checkpoint, status, and metric evidence written to standardized trainoutputs/. Do not use for environment setup, exploratory sweeps, speculative idea implementation, or end-to-end orchestration.

Testing & QA337.8K installs

test-driven-development

obra/superpowers

Use when implementing any feature or bugfix, before writing implementation code

Testing & QA261.8K installs

Use when implementation is complete, all tests pass, and you need to decide how to integrate the work

Testing & QA216.8K installs