github.com/ezbercih/terraform@v0.1.1-0.20140729011846-3c33865e0839/helper/resource/testing.go (about) 1 package resource 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "log" 8 "os" 9 "strings" 10 "testing" 11 12 "github.com/hashicorp/terraform/config" 13 "github.com/hashicorp/terraform/terraform" 14 ) 15 16 const TestEnvVar = "TF_ACC" 17 18 // TestCheckFunc is the callback type used with acceptance tests to check 19 // the state of a resource. The state passed in is the latest state known, 20 // or in the case of being after a destroy, it is the last known state when 21 // it was created. 22 type TestCheckFunc func(*terraform.State) error 23 24 // TestCase is a single acceptance test case used to test the apply/destroy 25 // lifecycle of a resource in a specific configuration. 26 // 27 // When the destroy plan is executed, the config from the last TestStep 28 // is used to plan it. 29 type TestCase struct { 30 // PreCheck, if non-nil, will be called before any test steps are 31 // executed. It will only be executed in the case that the steps 32 // would run, so it can be used for some validation before running 33 // acceptance tests, such as verifying that keys are setup. 34 PreCheck func() 35 36 // Provider is the ResourceProvider that will be under test. 37 Providers map[string]terraform.ResourceProvider 38 39 // CheckDestroy is called after the resource is finally destroyed 40 // to allow the tester to test that the resource is truly gone. 41 CheckDestroy TestCheckFunc 42 43 // Steps are the apply sequences done within the context of the 44 // same state. Each step can have its own check to verify correctness. 45 Steps []TestStep 46 } 47 48 // TestStep is a single apply sequence of a test, done within the 49 // context of a state. 50 // 51 // Multiple TestSteps can be sequenced in a Test to allow testing 52 // potentially complex update logic. In general, simply create/destroy 53 // tests will only need one step. 54 type TestStep struct { 55 // Config a string of the configuration to give to Terraform. 56 Config string 57 58 // Check is called after the Config is applied. Use this step to 59 // make your own API calls to check the status of things, and to 60 // inspect the format of the ResourceState itself. 61 // 62 // If an error is returned, the test will fail. In this case, a 63 // destroy plan will still be attempted. 64 // 65 // If this is nil, no check is done on this step. 66 Check TestCheckFunc 67 68 // Destroy will create a destroy plan if set to true. 69 Destroy bool 70 } 71 72 // Test performs an acceptance test on a resource. 73 // 74 // Tests are not run unless an environmental variable "TF_ACC" is 75 // set to some non-empty value. This is to avoid test cases surprising 76 // a user by creating real resources. 77 // 78 // Tests will fail unless the verbose flag (`go test -v`, or explicitly 79 // the "-test.v" flag) is set. Because some acceptance tests take quite 80 // long, we require the verbose flag so users are able to see progress 81 // output. 82 func Test(t TestT, c TestCase) { 83 // We only run acceptance tests if an env var is set because they're 84 // slow and generally require some outside configuration. 85 if os.Getenv(TestEnvVar) == "" { 86 t.Skip(fmt.Sprintf( 87 "Acceptance tests skipped unless env '%s' set", 88 TestEnvVar)) 89 return 90 } 91 92 // We require verbose mode so that the user knows what is going on. 93 if !testTesting && !testing.Verbose() { 94 t.Fatal("Acceptance tests must be run with the -v flag on tests") 95 return 96 } 97 98 // Run the PreCheck if we have it 99 if c.PreCheck != nil { 100 c.PreCheck() 101 } 102 103 // Build our context options that we can 104 ctxProviders := make(map[string]terraform.ResourceProviderFactory) 105 for k, p := range c.Providers { 106 ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) 107 } 108 opts := terraform.ContextOpts{Providers: ctxProviders} 109 110 // A single state variable to track the lifecycle, starting with no state 111 var state *terraform.State 112 113 // Go through each step and run it 114 for i, step := range c.Steps { 115 var err error 116 log.Printf("[WARN] Test: Executing step %d", i) 117 state, err = testStep(opts, state, step) 118 if err != nil { 119 t.Error(fmt.Sprintf( 120 "Step %d error: %s", i, err)) 121 break 122 } 123 } 124 125 // If we have a state, then run the destroy 126 if state != nil { 127 destroyStep := TestStep{ 128 Config: c.Steps[len(c.Steps)-1].Config, 129 Check: c.CheckDestroy, 130 Destroy: true, 131 } 132 133 log.Printf("[WARN] Test: Executing destroy step") 134 state, err := testStep(opts, state, destroyStep) 135 if err != nil { 136 t.Error(fmt.Sprintf( 137 "Error destroying resource! WARNING: Dangling resources\n"+ 138 "may exist. The full state and error is shown below.\n\n"+ 139 "Error: %s\n\nState: %s", 140 err, 141 state)) 142 } 143 } else { 144 log.Printf("[WARN] Skipping destroy test since there is no state.") 145 } 146 } 147 148 func testStep( 149 opts terraform.ContextOpts, 150 state *terraform.State, 151 step TestStep) (*terraform.State, error) { 152 // Write the configuration 153 cfgF, err := ioutil.TempFile("", "tf-test") 154 if err != nil { 155 return state, fmt.Errorf( 156 "Error creating temporary file for config: %s", err) 157 } 158 cfgPath := cfgF.Name() + ".tf" 159 cfgF.Close() 160 os.Remove(cfgF.Name()) 161 162 cfgF, err = os.Create(cfgPath) 163 if err != nil { 164 return state, fmt.Errorf( 165 "Error creating temporary file for config: %s", err) 166 } 167 defer os.Remove(cfgPath) 168 169 _, err = io.Copy(cfgF, strings.NewReader(step.Config)) 170 cfgF.Close() 171 if err != nil { 172 return state, fmt.Errorf( 173 "Error creating temporary file for config: %s", err) 174 } 175 176 // Parse the configuration 177 config, err := config.Load(cfgPath) 178 if err != nil { 179 return state, fmt.Errorf( 180 "Error parsing configuration: %s", err) 181 } 182 183 // Build the context 184 opts.Config = config 185 opts.State = state 186 ctx := terraform.NewContext(&opts) 187 if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { 188 estrs := make([]string, len(es)) 189 for i, e := range es { 190 estrs[i] = e.Error() 191 } 192 return state, fmt.Errorf( 193 "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", 194 ws, estrs) 195 } 196 197 // Refresh! 198 state, err = ctx.Refresh() 199 if err != nil { 200 return state, fmt.Errorf( 201 "Error refreshing: %s", err) 202 } 203 204 // Plan! 205 if p, err := ctx.Plan(&terraform.PlanOpts{Destroy: step.Destroy}); err != nil { 206 return state, fmt.Errorf( 207 "Error planning: %s", err) 208 } else { 209 log.Printf("[WARN] Test: Step plan: %s", p) 210 } 211 212 // Apply! 213 state, err = ctx.Apply() 214 if err != nil { 215 return state, fmt.Errorf("Error applying: %s", err) 216 } 217 218 // Check! Excitement! 219 if step.Check != nil { 220 if err = step.Check(state); err != nil { 221 err = fmt.Errorf("Check failed: %s", err) 222 } 223 } 224 225 return state, err 226 } 227 228 // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into 229 // a single TestCheckFunc. 230 // 231 // As a user testing their provider, this lets you decompose your checks 232 // into smaller pieces more easily. 233 func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { 234 return func(s *terraform.State) error { 235 for _, f := range fs { 236 if err := f(s); err != nil { 237 return err 238 } 239 } 240 241 return nil 242 } 243 } 244 245 func TestCheckResourceAttr(name, key, value string) TestCheckFunc { 246 return func(s *terraform.State) error { 247 rs, ok := s.Resources[name] 248 if !ok { 249 return fmt.Errorf("Not found: %s", name) 250 } 251 252 if rs.Attributes[key] != value { 253 return fmt.Errorf( 254 "%s: Attribute '%s' expected %#v, got %#v", 255 name, 256 key, 257 value, 258 rs.Attributes[key]) 259 } 260 261 return nil 262 } 263 } 264 265 // TestT is the interface used to handle the test lifecycle of a test. 266 // 267 // Users should just use a *testing.T object, which implements this. 268 type TestT interface { 269 Error(args ...interface{}) 270 Fatal(args ...interface{}) 271 Skip(args ...interface{}) 272 } 273 274 // This is set to true by unit tests to alter some behavior 275 var testTesting = false