provider-test-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseProvider Acceptance Test Patterns
Provider验收测试模式
Patterns for writing acceptance tests using
terraform-plugin-testing
with the Plugin Framework.
Source: HashiCorp Testing Patterns
References (load when needed):
- — statecheck, plancheck, knownvalue types, tfjsonpath, comparers
references/checks.md - — sweeper setup, TestMain, dependencies
references/sweepers.md - — ephemeral resource testing, echoprovider, multi-step patterns
references/ephemeral.md
基于Plugin Framework,使用terraform-plugin-testing编写验收测试的模式。
参考文档(按需查阅):
- — statecheck、plancheck、knownvalue类型、tfjsonpath、比较器
references/checks.md - — 清理器设置、TestMain、依赖项
references/sweepers.md - — 临时资源测试、echoprovider、多步骤模式
references/ephemeral.md
Test Lifecycle
测试生命周期
The framework runs each TestStep through: plan → apply → refresh → final
plan. If the final plan shows a diff, the test fails (unless
is set). After all steps, destroy runs followed by
. This means every test automatically verifies that
configurations apply cleanly and produce no drift — no assertions needed for
that.
ExpectNonEmptyPlanCheckDestroy框架会按以下流程执行每个TestStep:plan(计划)→ apply(应用)→ refresh(刷新)→ final plan(最终计划)。如果最终计划显示差异,测试将失败(除非设置了)。所有步骤完成后,会执行destroy(销毁)操作,随后运行。这意味着每个测试都会自动验证配置是否能正常应用且无漂移——无需额外编写断言。
ExpectNonEmptyPlanCheckDestroyTest 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 by default. Use only when tests
share state or cannot run concurrently.
resource.ParallelTestresource.Testgo
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()),
},
},
},
})
}默认使用。仅当测试需要共享状态或无法并发运行时,才使用。
resource.ParallelTestresource.TestProvider Factory
Provider工厂
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")()),
}go
// provider_test.go — 基于Protocol 6的Plugin Framework(按需使用Protocol5变体)
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"example": providerserver.NewProtocol6WithError(New("test")()),
}TestCase Fields
TestCase字段
| Field | Purpose |
|---|---|
| |
| Plugin Framework provider factories |
| |
| |
| |
| 字段 | 用途 |
|---|---|
| |
| Plugin Framework的Provider工厂 |
| |
| |
| |
TestStep Fields
TestStep字段
Config Mode
配置模式
| Field | Purpose |
|---|---|
| Inline HCL string to apply |
| |
| |
| |
| |
| |
| |
| |
| 字段 | 用途 |
|---|---|
| 要应用的内联HCL字符串 |
| |
| |
| |
| |
| |
| |
| |
Import Mode
导入模式
| Field | Purpose |
|---|---|
| |
| Verify imported state matches prior state |
| |
| |
| Resource address to import |
| Override the ID used for import |
| 字段 | 用途 |
|---|---|
| 设置为 |
| 验证导入后的状态与之前的状态一致 |
| |
| |
| 要导入的资源地址 |
| 覆盖导入时使用的ID |
Check Functions
检查函数
Modern: ConfigStateChecks (preferred)
现代方式:ConfigStateChecks(推荐)
Type-safe with aggregated error reporting. Compose built-in checks with custom
implementations. See for full
knownvalue types, tfjsonpath navigation, and comparers.
statecheck.StateCheckreferences/checks.mdgo
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 (legacy) and in the same step.
CheckConfigStateChecks类型安全且支持聚合错误报告。可将内置检查与自定义实现组合使用。完整的knownvalue类型、tfjsonpath导航和比较器请查阅。
statecheck.StateCheckreferences/checks.mdgo
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")),
},不要在同一个步骤中混合使用(旧版)和。
CheckConfigStateChecksLegacy: Check (for CheckDestroy and migration)
旧版:Check(用于CheckDestroy和迁移场景)
CheckDestroyTestCaseTestCheckFuncCheckTestStepTestCheckFuncConfigStateChecksgo
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"),
),ComposeAggregateTestCheckFuncComposeTestCheckFuncTestCaseCheckDestroyTestCheckFuncTestStepCheckTestCheckFuncConfigStateChecksgo
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"),
),ComposeAggregateTestCheckFuncComposeTestCheckFuncConfig Helpers
配置助手
Use numbered format verbs — for quoted strings, for raw:
%[1]q%[1]sgo
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)
}使用带编号的格式化动词 — 用于带引号的字符串, 用于原始字符串:
%[1]q%[1]sgo
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")),
},
},
},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
for import block generation:
ImportStateKindgo
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateKind: resource.ImportBlockWithID,
},在配置步骤完成后,验证导入操作是否能生成一致的状态。使用生成导入块:
ImportStateKindgo
{
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,
},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`),
},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()),
},
},
},
})
}正确的Bug修复至少需要两次提交:第一次提交回归测试(测试失败,确认Bug存在),然后提交修复代码(测试通过)。这样评审人员可以通过检出第一次提交独立验证测试是否能复现问题,再切换到修复版本确认问题已解决。
回归测试的命名和文档应明确说明其修复的问题。尽可能包含原始Bug报告的链接。
go
// TestAccExample_regressionGH1234 验证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{
{
// 复现问题:此配置会触发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
自定义StateCheck:资源存在验证
Implement for API existence verification. Separate the
exists check into its own function for reuse across steps — the source
recommends this as a design principle:
statecheck.StateCheckgo
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}
}实现用于通过API验证资源是否存在。将存在性检查拆分为独立函数以便在多个步骤中复用——这是官方推荐的设计原则:
statecheck.StateCheckgo
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
自定义StateCheck:资源消失验证
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}
}通过API删除资源以模拟外部删除场景:
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)
}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)
销毁检查(TestCheckFunc — 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
}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")
}
}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")
}
}