provider-test-patterns

작성자: hashicorp

Plugin Framework와 함께 terraform-plugin-testing을 사용하여 승인 테스트를 작성하기 위한 패턴입니다.

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

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")
    }
}

hashicorp의 다른 스킬

provider-framework-migration
hashicorp
Terraform 제공자 리소스와 데이터 소스를 Plugin SDKv2에서 Plugin Framework로 마이그레이션: 하나의 제공자에서 두 플러그인을 멀티플렉싱(terraform-plugin-mux, …)
provider-configuration
hashicorp
Terraform provider 구성 및 인증을 Plugin Framework로 구현: 자격 증명을 위한 provider 스키마(Optional + Sensitive 속성),…
provider-ephemeral-resources
hashicorp
Terraform 공급자 임시 리소스를 Plugin Framework로 구현: Open/Renew/Close 수명주기, 임시 스키마 설계, 등록 방법…
terraform-test
hashicorp
Terraform 테스트 작성 및 실행을 위한 종합 가이드로, 어설션, 모킹, 모듈 검증을 포함합니다. .tftest.hcl 구문을 사용하여 테스트 파일을 작성하며, plan 또는 apply 모드로 실행되는 run 블록을 지원하고, 선택적 상태 격리와 함께 순차 및 병렬 실행을 지원합니다. 리소스 속성, 출력, 데이터 소스에 대한 조건을 어설션하고, expect_failures를 사용하여 잘못된 입력이 적절히 거부되는지 검증합니다. Mock 제공자(Terraform 1.7.0+)는 인프라 동작을 시뮬레이션합니다...
terraform-policy
hashicorp
Terraform Policy 파일(.policy.hcl, .policytest.hcl, Sentinel→tfpolicy)을 작성, 테스트 또는 변환합니다. 트리거: policy.hcl, policytest, convert sentinel, tfpolicy,…
terraform-search-import
hashicorp
Terraform Search 쿼리를 사용하여 기존 클라우드 리소스를 검색하고 이를 Terraform 관리로 대량 가져옵니다. 관리되지 않는 인프라를 도입할 때 사용합니다…
aws-ami-builder
hashicorp
Packer의 amazon-ebs 빌더로 사용자 지정 Amazon 머신 이미지를 구축합니다. HCL 템플릿과 프로비저너(셸 스크립트, 파일 업로드, 구성 관리)를 사용해 소스 AMI에서 AMI 생성을 자동화합니다. ami_regions를 통한 다중 리전 AMI 배포와 이름, 소유자, 가상화 유형별 유연한 소스 AMI 필터링을 지원합니다. 환경 변수, AWS 자격 증명 파일 또는 IAM 인스턴스 프로파일을 통해 인증하며 템플릿 검증 및 빌드 명령을 포함합니다...
tfctl
hashicorp
tfctl CLI를 사용하여 HCP Terraform / Terraform Cloud / Terraform Enterprise와 상호작용합니다. 전체 API를 지원합니다. 모든 HCP Terraform 또는 Terraform Cloud 또는…에 사용하세요.