provider-test-patterns

Шаблоны для написания приемочных тестов с использованием terraform-plugin-testing и Plugin Framework.

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

Больше skills от hashicorp

provider-actions
hashicorp
Реализуйте действия провайдера Terraform с помощью Plugin Framework. Используйте при разработке императивных операций, которые выполняются на этапах жизненного цикла (до/после…
official
new-terraform-provider
hashicorp
Use this when scaffolding a new Terraform provider with the Plugin Framework: workspace layout, go module setup, provider server main.go, and a provider.go…
official
terraform-test
hashicorp
Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating…
official
terraform-test
hashicorp
Полное руководство по написанию и запуску тестов Terraform с утверждениями, мокированием и валидацией модулей. Создавайте тестовые файлы с использованием синтаксиса .tftest.hcl и блоков run, которые выполняются в режиме plan или apply, поддерживая последовательное и параллельное выполнение с опциональной изоляцией состояния. Утверждайте условия для атрибутов ресурсов, выходных данных и источников данных; используйте expect_failures для проверки корректного отклонения недопустимых входных данных. Мокируйте провайдеры (Terraform 1.7.0+) для симуляции поведения инфраструктуры без...
official
provider-actions
hashicorp
Реализация императивных действий провайдера Terraform в событиях жизненного цикла ресурсов с использованием Plugin Framework. Поддерживает триггеры жизненного цикла до/после создания и до/после обновления (события уничтожения недоступны в Terraform 1.14.0). Требует правильного определения схемы с корректными типами фреймворка, ElementType для коллекций и валидаторами для проверки входных данных. Включает отчетность о прогрессе, управление таймаутами и всестороннюю обработку ошибок для длительных операций. Реализует опрос и...
official
aws-ami-builder
hashicorp
Создаёт пользовательские образы Amazon Machine Images с помощью сборщика amazon-ebs из Packer. Автоматизирует создание AMI из исходных AMI с использованием HCL-шаблонов и провизионеров для настройки (скрипты оболочки, загрузка файлов, управление конфигурацией). Поддерживает многрегиональное распространение AMI через ami_regions и гибкую фильтрацию исходных AMI по имени, владельцу и типу виртуализации. Аутентифицируется через переменные окружения, файл учётных данных AWS или профили экземпляров IAM; включает команды проверки и сборки для шаблона...
official
new-terraform-provider
hashicorp
Создание каркаса нового провайдера Terraform с использованием Plugin Framework. Генерирует новую рабочую область модуля Go с стандартным соглашением об именовании "terraform-provider-" и инициализирует необходимые зависимости. Предоставляет шаблонный файл main.go, следующий шаблонам Plugin Framework от HashiCorp, с маркерами TODO для настройки. Проверяет настройку, выполняя команды сборки и тестирования, чтобы убедиться, что провайдер компилируется и проходит начальные проверки. Управляет рабочей областью, запрашивая подтверждение намерения перед созданием новой...
official
azure-verified-modules
hashicorp
Требования к сертификации и лучшие практики для модулей Azure Terraform, стремящихся к соответствию AVM. Обеспечивает соблюдение ограничений версий провайдера (azurerm >= 4.0, < 5.0; azapi >= 2.0, < 3.0) и запрещает ссылки на модули на основе git в пользу закрепленных источников реестра Terraform. Требует использования нижнего регистра с подчеркиваниями для всех идентификаторов, точных типов переменных, отдельных атрибутов вывода через шаблон антикоррупционного слоя и упорядоченных по алфавиту локальных переменных. Требует переменных переключателей функций для добавляемых новых ресурсов...
official