Agent Native

provider-test-patterns

This skill covers acceptance test patterns for Terraform providers using terraform-plugin-testing with the Plugin Framework.

by HashiCorpOfficial publisherNot reviewed

Identity proven by GitHub's verified organisation badge (hashicorp)

Agents
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, claude.ai and API
License
AGPL-3.0 (copyleft)
Pin
f706481pinned Oct 9, 2026
Context cost
about 3,000 tokenswhen the skill loads
GitHub stars
890on hashicorp/agent-skills
Scripts
None found

What it does

“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.”
The author's description, from SKILL.md

Generated summary from the skill's own text

This skill covers acceptance test patterns for Terraform providers using terraform-plugin-testing with the Plugin Framework. It documents TestCase and TestStep configuration, state and plan checks, custom check implementations, import testing, and common test scenarios like basic operations, updates, disappears, validation, and regression tests.

When to use it

  • Writing new Terraform provider acceptance tests with Plugin Framework
  • Reviewing or debugging existing provider test implementations
  • Implementing custom statecheck or plancheck validations
  • Setting up import state verification or ephemeral resource tests

When not to

  • Testing non-Terraform providers or legacy SDKv2 frameworks
  • Writing unit tests or integration tests outside Terraform ecosystem

Install

These commands install from the publisher's repository and need no account.

skills CLI

npx skills add hashicorp/agent-skills --skill provider-test-patterns

Installs from the repository's default branch, which may have changed since the commit we checked.

GitHub CLI 2.90 or later

gh skill install hashicorp/agent-skills provider-test-patterns@f706481af9b8fedb66de909f6243ad29601afa0c

Installs the commit we checked (f706481).

Copy by hand

Download the folder at f706481 and put it in .claude/skills/provider-test-patterns for Claude Code and in .agents/skills/provider-test-patterns for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.

Skill folder on GitHub at f706481

Copy for your agent

A prompt for any coding agent. It installs this skill from GitHub at the pinned commit, copies the files as they are, and runs nothing unless you ask.

Prompt

Install the agent skill "provider-test-patterns" by HashiCorp.

Source, pinned to commit f706481af9b8fedb66de909f6243ad29601afa0c:
https://github.com/hashicorp/agent-skills/tree/f706481af9b8fedb66de909f6243ad29601afa0c/plugins/terraform/skills/provider-test-patterns

1. Download that folder at exactly this commit. Do not use the default branch.
2. Copy every file unchanged, keeping the folder structure, into .claude/skills/provider-test-patterns/ for Claude Code and .agents/skills/provider-test-patterns/ for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. Create the folders if they do not exist.
3. Do not edit the files, and do not run any script in the folder unless I ask you to.
4. When you are done, list the files you copied and where you put them.

Listing and checks: https://www.agentnative.dev/skills/hashicorp/provider-test-patterns

SKILL.md at the pin

---
name: provider-test-patterns
description: >-
  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.
metadata:
  lifecycle-status: active
  copyright: Copyright IBM Corp. 2026
  version: "0.0.1"
---

# Provider Acceptance Test Patterns

Patterns for writing acceptance tests using
[terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing)
with the [Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework).

Source: [HashiCorp Testing Patterns](https://developer.hashicorp.com/terraform/plugin/testing/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

```go
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

```go
// 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.

```go
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.

```go
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:

```go
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)

```go
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:

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

### Disappears (resource deleted externally)

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

### Validation (expect error)

```go
{
    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.

```go
// 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:

```go
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:

```go
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)

```go
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)

```go
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

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

From hashicorp/agent-skills at f706481, shown as plain text under its license: AGPL-3.0 (copyleft). The license text is in LICENSE.

View SKILL.md on GitHub at f706481

Files

4 files, 28 KB in total. Each file has a sha256 in the pin's manifest; the first 12 characters are shown.

Skill folder on GitHub at f706481
All 19 skills from HashiCorp

More from HashiCorp

  • aws-ami-builder

    This skill enables the agent to build custom Amazon Machine Images using Packer's amazon-ebs builder.

    Claude Code, Codex, Cursor +4 · about 870 tokens · pinned 6 days ago

  • azure-image-builder

    This skill uses Packer's Azure ARM builder to create custom managed images and Compute Gallery images in Azure.

    Claude Code, Codex, Cursor +4 · about 1K tokens · pinned 6 days ago

  • azure-verified-modules

    This skill provides comprehensive requirements and best practices for developing Azure Terraform modules that meet Azure Verified Modules (AVM) certification…

    Claude Code, Codex, Cursor +4 · about 4K tokens · pinned 6 days ago

  • new-terraform-provider

    Scaffolds a new Terraform provider using the Plugin Framework by creating workspace structure, initializing a Go module, and generating main.go and…

    Contains scripts

    Claude Code, Codex, Cursor +3 · about 330 tokens · pinned 6 days ago

  • provider-actions

    This skill guides implementing Terraform Actions, imperative operations that execute at provider lifecycle events using the Plugin Framework.

    Claude Code, Codex, Cursor +4 · about 4.2K tokens · pinned 6 days ago

  • provider-configuration

    This skill guides implementing Terraform provider authentication using the Plugin Framework, covering provider schema design, credential provider chains that…

    Claude Code, Codex, Cursor +4 · about 3.3K tokens · pinned 6 days ago

All Testing and QA skills
  • run-acceptance-tests

    This skill guides the agent to run and diagnose Terraform provider acceptance tests, which are Go test functions prefixed with TestAcc that provision real…

    by HashiCorpOfficial publisher

    Claude Code, Codex, Cursor +4 · about 660 tokens · pinned 6 days ago

  • terraform-test

    This skill teaches agents to write and execute Terraform tests using .tftest.hcl files with run blocks and assertions.

    by HashiCorpOfficial publisher

    Claude Code, Codex, Cursor +4 · about 2.7K tokens · pinned 6 days ago

  • next-partial-prefetching-optimizer

    This skill optimizes Next.js client-side navigation under Partial Prefetching by defining what UI loads before and after a click.

    by VercelOfficial publisher

    Claude Code, Codex, Cursor +4 · about 2.8K tokens · pinned 6 days ago

  • Invalid--Name

    This skill cannot be described because its name format is invalid and no functional description or instructions are provided in the skill metadata.

    by CrewAIOfficial publisher

    Claude Code, Codex, Cursor +4 · about 0 tokens · pinned 6 days ago

  • chrome-devtools-cli

    This skill enables agents to automate browser interactions and inspect web pages using Chrome DevTools via command-line interface.

    by GoogleOfficial publisher

    Claude Code, Codex, Cursor +4 · about 2.9K tokens · pinned 6 days ago

  • triaging-visual-review-runs

    Inspects PostHog Visual Review screenshot regression runs that gate pull requests, helping engineers triage diff results, identify flaky stories, and decide…

    by PostHogOfficial publisher

    Claude Code, Codex, Cursor +4 · about 8.9K tokens · pinned 6 days ago

The skills listed here are third-party software, collected from public GitHub repositories as a catalog of recent skills and provided as is. Agent Native does not write or control them and is not responsible for any harm, loss or damage caused by installing or using them. Labels and checks describe what we saw at one commit and are not a guarantee. Read a skill's files before you run it. Terms of use