Acceptance tests are expressed in terms of Test Cases, each using one or
more Terraform configurations designed to create a set of resources under test,
and then verify the actual infrastructure created. Terraform’s resource
package offers a method Test(), accepting two parameters and acting as the
entry point to Terraform’s acceptance test framework. The first parameter is the
standard *testing.T struct from Golang’s Testing package, and the second is
TestCase, a Go struct that developers use to setup the acceptance tests.
Here’s an example acceptance test. Here the Provider is named Example, and the
Resource under test is Widget. The parts of this test are explained below the
example.
package example
// example.Widget represents a concrete Go type that represents an API resourcefuncTestAccExampleWidget_basic(t *testing.T){var widgetBefore, widgetAfter example.Widget
rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
resource.Test(t, resource.TestCase{
PreCheck:func(){testAccPreCheck(t)},
Providers: testAccProviders,
CheckDestroy: testAccCheckExampleResourceDestroy,
Steps:[]resource.TestStep{{
Config:testAccExampleResource(rName),
Check: resource.ComposeTestCheckFunc(testAccCheckExampleResourceExists("example_widget.foo",&widgetBefore),),},{
Config:testAccExampleResource_removedPolicy(rName),
Check: resource.ComposeTestCheckFunc(testAccCheckExampleResourceExists("example_widget.foo",&widgetAfter),),},},})}
package example
// example.Widget represents a concrete Go type that represents an API resourcefuncTestAccExampleWidget_basic(t *testing.T){var widgetBefore, widgetAfter example.Widget
rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resource.Test(t, resource.TestCase{ PreCheck:func(){testAccPreCheck(t)}, Providers: testAccProviders, CheckDestroy: testAccCheckExampleResourceDestroy, Steps:[]resource.TestStep{{ Config:testAccExampleResource(rName), Check: resource.ComposeTestCheckFunc(testAccCheckExampleResourceExists("example_widget.foo",&widgetBefore),),},{ Config:testAccExampleResource_removedPolicy(rName), Check: resource.ComposeTestCheckFunc(testAccCheckExampleResourceExists("example_widget.foo",&widgetAfter),),},},})}
Terraform acceptance tests are declared with the naming pattern TestAccXxx
and with the standard Go test function signature of func TestAccXxx(*testing.T).
Using the above test as an example:
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){// ...}
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){// ...}
Inside this function we invoke resource.Test() with the *testing.T input and
a new testcase object:
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){
resource.Test(t, resource.TestCase{// ...})}
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){ resource.Test(t, resource.TestCase{// ...})}
The majority of acceptance tests will only invoke resource.Test() and exit. If
at any point this method encounters an error, either in executing the provided
Terraform configurations or subsequent developer defined checks, Test() will
invoke the t.Error method of Go’s standard testing framework and the test will
fail. A failed test will not halt or otherwise interrupt any other tests
currently running.
TestCase offers several fields for developers to add to customize and validate
each test, defined below. The source for TestCase can be viewed here on
godoc.org
IsUnitTest allows a test to run regardless of the TF_ACC environment
variable. This should be used with care - only for fast tests on local resources
(e.g. remote state with a local backend) but can be used to increase confidence
in correct operation of Terraform without waiting for a full acceptance test
run.
PreCheck if non-nil, will be called before any test steps are executed. It
is commonly used to verify that required values exist for testing, such as
environment variables containing test keys that are used to configure the
Provider or Resource under test.
Example usage:
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){
resource.Test(t, resource.TestCase{
PreCheck:func(){testAccPreCheck(t)},// ...})}// testAccPreCheck validates the necessary test API keys exist// in the testing environmentfunctestAccPreCheck(t *testing.T){if v := os.Getenv("EXAMPLE_KEY"); v ==""{
t.Fatal("EXAMPLE_KEY must be set for acceptance tests")}if v := os.Getenv("EXAMPLE_SECRET"); v ==""{
t.Fatal("EXAMPLE_SECRET must be set for acceptance tests")}}
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){ resource.Test(t, resource.TestCase{ PreCheck:func(){testAccPreCheck(t)},// ...})}// testAccPreCheck validates the necessary test API keys exist// in the testing environmentfunctestAccPreCheck(t *testing.T){if v := os.Getenv("EXAMPLE_KEY"); v ==""{ t.Fatal("EXAMPLE_KEY must be set for acceptance tests")}if v := os.Getenv("EXAMPLE_SECRET"); v ==""{ t.Fatal("EXAMPLE_SECRET must be set for acceptance tests")}}
Providers is a map of *schema.Provider values with string
keys, representing the Providers that will be under test. Only the Providers
included in this map will be loaded during the test, so any Provider included in
a configuration file for testing must be represented in this map or the test
will fail during initialization.
This map is most commonly constructed once in a common init() method of the
Provider’s main test file, and includes an object of the current Provider type.
Example usage: (note the different files widget_test.go and provider_test.go)
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){
resource.Test(t, resource.TestCase{
PreCheck:func(){testAccPreCheck(t)},
Providers: testAccProviders,// ...})}// File: example/provider_test.gopackage example
var testAccProviders map[string]*schema.Provider
var testAccProvider *schema.Provider
funcinit(){
testAccProvider =Provider()
testAccProviders =map[string]*schema.Provider{"example": testAccProvider,}}
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){ resource.Test(t, resource.TestCase{ PreCheck:func(){testAccPreCheck(t)}, Providers: testAccProviders,// ...})}// File: example/provider_test.gopackage example
var testAccProviders map[string]*schema.Provider
var testAccProvider *schema.Provider
funcinit(){ testAccProvider =Provider() testAccProviders =map[string]*schema.Provider{"example": testAccProvider,}}
CheckDestroy is called after all test steps have been run and Terraform
has run destroy on the remaining state. This allows developers to ensure any
resource created is truly destroyed. This method receives the last known
Terraform state as input, and commonly uses infrastructure SDKs to query APIs
directly to verify the expected objects are no longer found, and should return
an error if any resources remain.
Example usage:
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){
resource.Test(t, resource.TestCase{
PreCheck:func(){testAccPreCheck(t)},
Providers: testAccProviders,
CheckDestroy: testAccCheckExampleResourceDestroy,// ...})}// testAccCheckExampleResourceDestroy verifies the Widget// has been destroyedfunctestAccCheckExampleResourceDestroy(s *terraform.State)error{// retrieve the connection established in Provider configuration
conn := testAccProvider.Meta().(*ExampleClient)// loop through the resources in state, verifying each widget// is destroyedfor_, rs :=range s.RootModule().Resources {if rs.Type !="example_widget"{continue}// Retrieve our widget by referencing it's state ID for API lookup
request :=&example.DescribeWidgets{
IDs:[]string{rs.Primary.ID},}
response, err := conn.DescribeWidgets(request)if err ==nil{iflen(response.Widgets)>0&&*response.Widgets[0].ID == rs.Primary.ID {return fmt.Errorf("Widget (%s) still exists.", rs.Primary.ID)}returnnil}// If the error is equivalent to 404 not found, the widget is destroyed.// Otherwise return the errorif!strings.Contains(err.Error(),"Widget not found"){return err
}}returnnil}
// File: example/widget_test.gopackage example
funcTestAccExampleWidget_basic(t *testing.T){ resource.Test(t, resource.TestCase{ PreCheck:func(){testAccPreCheck(t)}, Providers: testAccProviders, CheckDestroy: testAccCheckExampleResourceDestroy,// ...})}// testAccCheckExampleResourceDestroy verifies the Widget// has been destroyedfunctestAccCheckExampleResourceDestroy(s *terraform.State)error{// retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*ExampleClient)// loop through the resources in state, verifying each widget// is destroyedfor_, rs :=range s.RootModule().Resources {if rs.Type !="example_widget"{continue}// Retrieve our widget by referencing it's state ID for API lookup request :=&example.DescribeWidgets{ IDs:[]string{rs.Primary.ID},} response, err := conn.DescribeWidgets(request)if err ==nil{iflen(response.Widgets)>0&&*response.Widgets[0].ID == rs.Primary.ID {return fmt.Errorf("Widget (%s) still exists.", rs.Primary.ID)}returnnil}// If the error is equivalent to 404 not found, the widget is destroyed.// Otherwise return the errorif!strings.Contains(err.Error(),"Widget not found"){return err
}}returnnil}
TestStep is a single apply sequence of a test, done within the context of a
state. Multiple TestSteps can be sequenced in a Test to allow testing
potentially complex update logic and usage. Basic tests typically contain one to
two steps, to verify the resource can be created and subsequently updated,
depending on the properties of the resource. In general, simply create/destroy
tests will only need one step.
TestCases are used to verify the features of a given part of a plugin. Each
case should represent a scenario of normal usage of the plugin, from simple
creation to creating, adding, and removing specific properties. In the next
Section TestSteps, we’ll detail Steps portion of TestCase and see how
to create these scenarios by iterating on Terraform configurations.