How to fail acceptance test when RequiresReplace() is triggered?

I’d like my resource acceptance tests to validate that a configuration changes between TestSteps are correctly triggering (or not triggering) resource replacement.

I’m using terraform-plugin-testing v1.7.0.

Is there a TestStep lever which can control this? I’ve noticed Destroy, but I can’t seem to make sense of that one.

Alternatively, is there a Check pattern which can recognize (possibly via closure?) when the ID attribute is churning between TestSteps?

Hi @hQnVyLRx :wave:

One option you have available to you is to use the ExpectResourceAction plan check.

For example, with the following provider code:

var _ resource.Resource = (*playgroundResource)(nil)

type playgroundResource struct {
}

func NewResource() resource.Resource {
	return &playgroundResource{}
}

func (e *playgroundResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_resource"
}

func (e *playgroundResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"configurable_attribute": schema.StringAttribute{
				Optional: true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.RequiresReplace(), // Triggers destroy-create whenever this value is altered.
				},
			},
			"id": schema.StringAttribute{
				Computed: true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.UseStateForUnknown(),
				},
			},
		},
	}
}

type playgroundResourceData struct {
	ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
	Id                    types.String `tfsdk:"id"`
}

func (e *playgroundResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data playgroundResourceData

	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	data.Id = types.StringValue(time.Now().String())

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

func (e *playgroundResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	var data playgroundResourceData

	diags := req.State.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

func (e *playgroundResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
	var data playgroundResourceData

	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

func (e *playgroundResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	var data playgroundResourceData

	diags := req.State.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}
}

You could use the ExpectResourceAction plan check as follows:

func TestAccExampleResource(t *testing.T) {
	resource.Test(t, resource.TestCase{
		PreCheck:                 func() { testAccPreCheck(t) },
		ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
		Steps: []resource.TestStep{
			{
				Config: testAccExampleResourceConfig("one"),
				ConfigPlanChecks: resource.ConfigPlanChecks{
					PreApply: []plancheck.PlanCheck{
						plancheck.ExpectResourceAction("playground_resource.test", plancheck.ResourceActionCreate),
					},
				},
			},
			{
				Config: testAccExampleResourceConfig("two"),
				ConfigPlanChecks: resource.ConfigPlanChecks{
					PreApply: []plancheck.PlanCheck{
						plancheck.ExpectResourceAction("playground_resource.test", plancheck.ResourceActionDestroyBeforeCreate),
					},
				},
			},
		},
	})
}

func testAccExampleResourceConfig(configurableAttribute string) string {
	return fmt.Sprintf(`
resource "playground_resource" "test" {
  configurable_attribute = %[1]q
}
`, configurableAttribute)
}

Alternatively, a frequently observed pattern in provider development is the extraction and comparison of values from state. For example (note that the following is simplified and omits checks that should be included, such as nil value checks):

func TestAccExampleResource(t *testing.T) {
	var idOne, idTwo string

	resource.Test(t, resource.TestCase{
		PreCheck:                 func() { testAccPreCheck(t) },
		ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
		Steps: []resource.TestStep{
			{
				Config: testAccExampleResourceConfig("one"),
				Check: resource.ComposeTestCheckFunc(
					extractValueFromTerraformState("playground_resource.test", "id", &idOne),
				),
			},
			{
				Config: testAccExampleResourceConfig("two"),
				Check: resource.ComposeTestCheckFunc(
					extractValueFromTerraformState("playground_resource.test", "id", &idTwo),
					expectDiffer(&idOne, &idTwo),
				),
			},
		},
	})
}

func extractValueFromTerraformState(name string, id string, val *string) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		rs, ok := s.RootModule().Resources[name]
		if !ok {
			return fmt.Errorf("resource not found: %s", name)
		}

		idVal := rs.Primary.Attributes[id]

		*val = idVal

		return nil
	}
}

func expectDiffer(idOne, idTwo *string) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		if idOne == idTwo {
			return fmt.Errorf("expected %s and %s to differ, but they were the same", *idOne, *idTwo)
		}

		return nil
	}
}

func testAccExampleResourceConfig(configurableAttribute string) string {
	return fmt.Sprintf(`
resource "playground_resource" "test" {
  configurable_attribute = %[1]q
}
`, configurableAttribute)
}

Equivalent checks can be created that leverage the StateCheck interface. We are actively considering the implementation of built-in state checks for these sorts of purposes:

Thank you, @bendbennett!

ExpectResourceAction seems like it’s what I was looking for.

The second example, with extraction and comparison as part of a single composite check has also inspired me.

This was really helpful.