mcpField
P
provider-test-patterns

provider-test-patterns

github.com/hashicorpテスト・品質9093.3k 回インストール今日更新
リポジトリ

provider-test-patterns について

terraform-plugin-testing と Plugin Framework を使った Terraform プロバイダーの受け入れテストパターンを解説するガイドです。テストの流れ、状態チェック、インポートや更新、外部削除のシナリオを扱います。

主な機能

  1. 01受け入れテストのライフサイクル説明
  2. 02Plugin Framework のプロバイダーファクトリー設定
  3. 03ConfigStateChecks によるアサーション
  4. 04インポート、更新、検証のパターン
  5. 05破棄とリソース消失のチェック

活用シーン

  1. 01プロバイダー受け入れテストの構成
  2. 02apply 後のリソース状態の検証
  3. 03インポートと外部削除シナリオのテスト

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/provider-test-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
checks.md
ephemeral.md
sweepers.md

詳細

プロバイダーの受け入れテストパターン

Plugin Frameworkで terraform-plugin-testing を使用して受け入れテストを作成するためのパターンです。

ソース:HashiCorp Testing Patterns

参考資料(必要に応じて読み込んでください):

  • references/checks.md — statecheck、plancheck、knownvalue の型、tfjsonpath、比較関数
  • references/sweepers.md — sweeper のセットアップ、TestMain、依存関係
  • references/ephemeral.md — エフェメラルリソースのテスト、echoprovider、複数ステップのパターン

テストのライフサイクル

フレームワークは各 TestStep を plan → apply → refresh → final plan の順で実行します。最終プランに差分がある場合、テストは失敗します(ExpectNonEmptyPlan が設定されている場合を除く)。すべてのステップが終わると、destroy の後に CheckDestroy が実行されます。つまり、すべてのテストで構成が問題なく適用され、ドリフトが発生しないことが自動的に検証されるため、そのためのアサーションは不要です。


テスト関数の構成

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.ParallelTest を使用してください。状態を共有するテストや並行実行できないテストでのみ resource.Test を使用してください。


プロバイダーファクトリー

// 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 フィールド

フィールド目的
PreCheckfunc() — 前提条件(環境変数、API アクセス)を確認します
ProtoV6ProviderFactoriesPlugin Framework のプロバイダーファクトリー
CheckDestroyTestCheckFunc — すべてのステップの後にリソースが破棄されたことを確認します
Steps[]TestStep — 順次実行するテスト操作
TerraformVersionChecks[]tfversion.TerraformVersionCheck — CLI バージョンによる実行条件

TestStep フィールド

Config モード
フィールド目的
Config適用するインライン HCL 文字列
ConfigStateChecks[]statecheck.StateCheck — 最新のアサーション(推奨)
ConfigPlanChecksresource.ConfigPlanChecks{PreApply: []plancheck.PlanCheck{...}}
ExpectError*regexp.Regexp — パターンに一致する失敗を想定します
ExpectNonEmptyPlanbool — 適用後に空でないプランを想定します
PlanOnlybool — 適用せずにプランを作成します
Destroybool — destroy ステップを実行します
PreConfigfunc() — ステップの前にセットアップを行います
インポートモード
フィールド目的
ImportStatetrue にするとインポートモードが有効になります
ImportStateVerifyインポートした状態が前の状態と一致することを確認します
ImportStateVerifyIgnore[]string — 確認時にスキップする属性
ImportStateKindresource.ImportBlockWithID — インポートブロックの生成
ResourceNameインポートするリソースアドレス
ImportStateIdインポートに使用する ID を上書きします

チェック関数

最新の方法:ConfigStateChecks(推奨)

型安全で、エラーをまとめて報告できます。組み込みチェックとカスタムの statecheck.StateCheck 実装を組み合わせてください。knownvalue の全タイプ、tfjsonpath によるナビゲーション、比較関数については references/checks.md を参照してください。

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

同じステップで Check(従来方式)と ConfigStateChecks を併用しないでください。

従来方式:Check(CheckDestroy と移行用)

TestCase の CheckDestroy では TestCheckFunc が必要です。TestStep の Check フィールドも TestCheckFunc を受け付けますが、新しいテストでは ConfigStateChecks を優先してください。

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 はすべてのエラーを報告します。ComposeTestCheckFunc は最初のエラーで失敗します。


Config ヘルパー

番号付きフォーマット指定子を使用してください。引用符付き文字列には %[1]q、そのままの文字列には %[1]s を使います。

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つのテストにまとめます)
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")),
        },
    },
},
インポート

構成ステップの後に、インポートによって同一の状態が生成されることを確認します。インポートブロックの生成には ImportStateKind を使用してください。

{
    ResourceName:      resourceName,
    ImportState:       true,
    ImportStateVerify: true,
    ImportStateKind:   resource.ImportBlockWithID,
},
リソースが見つからない(リソースが外部で削除された場合)
{
    Config: testAccExampleConfig_basic(rName),
    ConfigStateChecks: []statecheck.StateCheck{
        stateCheckExampleExists(resourceName, &widget),
        stateCheckExampleDisappears(resourceName),
    },
    ExpectNonEmptyPlan: true,
},
検証(エラーを想定)
{
    Config:      testAccExampleConfig_invalidName(""),
    ExpectError: regexp.MustCompile(`name must not be empty`),
},
リグレッション(二段階コミットワークフロー)

適切なバグ修正では、少なくとも二つのコミットが必要です。まずリグレッションテストをコミットし(テストは失敗してバグを確認します)、次に修正をコミットします(テストは合格します)。こうすることで、レビュー担当者は最初のコミットをチェックアウトしてテストが問題を再現することを独自に確認し、その後、修正コミットまで進めて確認できます。

修正する問題を特定できるよう、リグレッションテストに名前を付け、内容を記述してください。可能な場合は、元のバグ報告へのリンクを含めてください。

// 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()),
                },
            },
        },
    })
}

ヘルパー関数

カスタム StateCheck:存在確認

API での存在確認を行う statecheck.StateCheck を実装します。手順間で再利用できるよう、存在確認を独自の関数に分けてください。これはソースで設計原則として推奨されています。

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}
}
カスタム StateCheck:消失確認

外部からの削除をシミュレートするため、API 経由でリソースを削除します。

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 リソースの検索(共有ユーティリティ)
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 チェック(TestCheckFunc — 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")
    }
}

コメント