diff --git a/helper/resource/testing.go b/helper/resource/testing.go index c50d6d120..9557207c3 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -151,6 +151,11 @@ type TestStep struct { // test to pass. ExpectError *regexp.Regexp + // PlanOnly can be set to only run `plan` with this configuration, and not + // actually apply it. This is useful for ensuring config changes result in + // no-op plans + PlanOnly bool + // PreventPostDestroyRefresh can be set to true for cases where data sources // are tested alongside real resources PreventPostDestroyRefresh bool diff --git a/helper/resource/testing_config.go b/helper/resource/testing_config.go index b49fdc794..537a11c34 100644 --- a/helper/resource/testing_config.go +++ b/helper/resource/testing_config.go @@ -53,34 +53,38 @@ func testStep( "Error refreshing: %s", err) } - // Plan! - if p, err := ctx.Plan(); err != nil { - return state, fmt.Errorf( - "Error planning: %s", err) - } else { - log.Printf("[WARN] Test: Step plan: %s", p) - } - - // We need to keep a copy of the state prior to destroying - // such that destroy steps can verify their behaviour in the check - // function - stateBeforeApplication := state.DeepCopy() - - // Apply! - state, err = ctx.Apply() - if err != nil { - return state, fmt.Errorf("Error applying: %s", err) - } - - // Check! Excitement! - if step.Check != nil { - if step.Destroy { - if err := step.Check(stateBeforeApplication); err != nil { - return state, fmt.Errorf("Check failed: %s", err) - } + // If this step is a PlanOnly step, skip over this first Plan and subsequent + // Apply, and use the follow up Plan that checks for perpetual diffs + if !step.PlanOnly { + // Plan! + if p, err := ctx.Plan(); err != nil { + return state, fmt.Errorf( + "Error planning: %s", err) } else { - if err := step.Check(state); err != nil { - return state, fmt.Errorf("Check failed: %s", err) + log.Printf("[WARN] Test: Step plan: %s", p) + } + + // We need to keep a copy of the state prior to destroying + // such that destroy steps can verify their behaviour in the check + // function + stateBeforeApplication := state.DeepCopy() + + // Apply! + state, err = ctx.Apply() + if err != nil { + return state, fmt.Errorf("Error applying: %s", err) + } + + // Check! Excitement! + if step.Check != nil { + if step.Destroy { + if err := step.Check(stateBeforeApplication); err != nil { + return state, fmt.Errorf("Check failed: %s", err) + } + } else { + if err := step.Check(state); err != nil { + return state, fmt.Errorf("Check failed: %s", err) + } } } } diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go index 634f30773..7c64f9eb8 100644 --- a/helper/resource/testing_test.go +++ b/helper/resource/testing_test.go @@ -120,6 +120,58 @@ func TestTest(t *testing.T) { } } +func TestTest_plan_only(t *testing.T) { + mp := testProvider() + mp.ApplyReturn = &terraform.InstanceState{ + ID: "foo", + } + + checkDestroy := false + + checkDestroyFn := func(*terraform.State) error { + checkDestroy = true + return nil + } + + mt := new(mockT) + Test(mt, TestCase{ + Providers: map[string]terraform.ResourceProvider{ + "test": mp, + }, + CheckDestroy: checkDestroyFn, + Steps: []TestStep{ + TestStep{ + Config: testConfigStr, + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) + + if !mt.failed() { + t.Fatal("test should've failed") + } + + expected := `Step 0 error: After applying this step, the plan was not empty: + +DIFF: + +CREATE: test_instance.foo + foo: "" => "bar" + +STATE: + +` + + if mt.failMessage() != expected { + t.Fatalf("Expected message: %s\n\ngot:\n\n%s", expected, mt.failMessage()) + } + + if !checkDestroy { + t.Fatal("didn't call check for destroy") + } +} + func TestTest_idRefresh(t *testing.T) { // Refresh count should be 3: // 1.) initial Ref/Plan/Apply