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"
)
}
}
provider-test-patterns
安装
npx skills add https://github.com/hashicorp/agent-skills --skill provider-test-patterns