diff --git a/.changes/unreleased/ENHANCEMENTS-20251014-162659.yaml b/.changes/unreleased/ENHANCEMENTS-20251014-162659.yaml new file mode 100644 index 000000000..5c13f4fc7 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20251014-162659.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'helper/resource: Adds `PostApplyFunc` test step hook to run generic post-apply logic for plan/apply testing.' +time: 2025-10-14T16:26:59.267372-04:00 +custom: + Issue: "566" diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 61c03ff72..4ba1961bf 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -671,6 +671,10 @@ type TestStep struct { // SkipFunc is called after PreConfig but before applying the Config. SkipFunc func() (bool, error) + // PostApplyFunc is called after the Config is applied and after all plan/apply checks are run. + // This can be used to perform assertions against API values that are not stored in Terraform state. + PostApplyFunc func() + //--------------------------------------------------------------- // ImportState testing //--------------------------------------------------------------- diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index eecc87ac0..1145c8e4e 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -451,6 +451,12 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } } + if step.PostApplyFunc != nil { + logging.HelperResourceDebug(ctx, "Calling TestCase PostApplyFunc") + step.PostApplyFunc() + logging.HelperResourceDebug(ctx, "Called TestCase PostApplyFunc") + } + if refreshAfterApply && !step.Destroy && !step.PlanOnly { if len(c.Steps) > stepIndex+1 { // If the next step is a refresh, then we have no need to refresh here diff --git a/helper/resource/testing_new_config_test.go b/helper/resource/testing_new_config_test.go index 147a24dfa..15cf5db0c 100644 --- a/helper/resource/testing_new_config_test.go +++ b/helper/resource/testing_new_config_test.go @@ -842,3 +842,67 @@ func Test_ConfigStateChecks_Errors(t *testing.T) { }, }) } + +func Test_PostApplyFunc_Called(t *testing.T) { + t.Parallel() + + spy1 := &stateCheckSpy{} + postFuncCalled := false + UnitTest(t, TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + spy1, + }, + PostApplyFunc: func() { + postFuncCalled = true + }, + }, + }, + }) + + if !postFuncCalled { + t.Error("expected PostApplyFunc to be called at least once") + } + + if !spy1.called { + t.Error("expected ConfigStateChecks spy1 to be called at least once") + } +}