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
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"),
Co