Terraform
Implement automated testing
In this tutorial, you will add automated acceptance testing capabilities to the data source and resource of a provider that interacts with the API of a fictional coffee-shop application called HashiCups. To do this, you will:
- Implement data source acceptance testing.
This automates the end-to-end testing of the data source. - Run data source acceptance testing.
This ensures that the data source testing works as expected. - Implement resource acceptance testing.
This automates the end-to-end testing of the resource. - Run resource acceptance testing.
This ensures that the resource testing works as expected. - Implement function acceptance testing.
This automates the end-to-end testing of the function. - Run function acceptance testing.
This ensures that the function testing works as expected.
The terraform-plugin-testing
Go module helper/resource
package enables providers
to implement automated acceptance testing. The testing framework is built on top
of standard go test
command functionality and calls actual Terraform commands,
such as terraform apply
, terraform import
, and terraform destroy
. Unlike
manual testing, you do not have to locally reinstall the provider on code
updates or switch directories to use the expected Terraform configuration when
you run the automated tests.
Prerequisites
To follow this tutorial, you need:
- Go 1.21+ installed and configured.
- Terraform v1.8+ installed locally.
- Docker and Docker Compose to run an instance of HashiCups locally.
Navigate to your terraform-provider-hashicups
directory.
Your code should match the 09-functions
directory
from the example repository.
If you're stuck at any point during this tutorial, refer to the
acceptance-tests
branch to see the changes implemented in this tutorial.
Implement data source acceptance testing
Data source acceptance testing verifies that the Terraform state contains data after being read from the API.
Most providers will manage some shared implementation details in a single testing file to simplify the data source and resource testing implementations.
Navigate to the internal/provider
directory and remove the example scaffolding test files.
$ cd internal/provider && rm example_data_source_test.go example_resource_test.go example_function_test.go
Open the internal/provider/provider_test.go
file and replace the existing code with the following.
internal/provider/provider_test.go
package provider
import (
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
)
const (
// providerConfig is a shared configuration to combine with the actual
// test configuration so the HashiCups client is properly configured.
// It is also possible to use the HASHICUPS_ environment variables instead,
// such as updating the Makefile and running the testing through that tool.
providerConfig = `
provider "hashicups" {
username = "education"
password = "test123"
host = "http://localhost:19090"
}
`
)
var (
// testAccProtoV6ProviderFactories are used to instantiate a provider during
// acceptance testing. The factory function will be invoked for every Terraform
// CLI command executed to create a provider server to which the CLI can
// reattach.
testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"hashicups": providerserver.NewProtocol6WithError(New("test")()),
}
)
Create a new internal/provider/coffees_data_source_test.go
file with the following.
internal/provider/coffees_data_source_test.go
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccCoffeesDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Read testing
{
Config: providerConfig + `data "hashicups_coffees" "test" {}`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify number of coffees returned
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.#", "9"),
// Verify the first coffee to ensure all attributes are set
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.description", ""),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.id", "1"),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.image", "/hashicorp.png"),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.ingredients.#", "1"),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.ingredients.0.id", "6"),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.name", "HCP Aeropress"),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.price", "200"),
resource.TestCheckResourceAttr("data.hashicups_coffees.test", "coffees.0.teaser", "Automation in a cup"),
),
},
},
})
}
Verify data source testing functionality
Now that you have implemented the testing functionality to the data source, you can run the tests.
Run Go testing with the TF_ACC
environment variable set. The test framework
will report that your data source's test passed.
$ TF_ACC=1 go test -count=1 -v
=== RUN TestAccCoffeesDataSource
--- PASS: TestAccCoffeesDataSource (1.23s)
PASS
ok terraform-provider-hashicups/internal/provider 2.120s
Implement resource testing functionality
Resource acceptance testing verifies that the entire resource lifecycle, such as
the Create
, Read
, Update
, and Delete
functionality, along with import
capabilities. The testing framework automatically handles destroying test
resources and returning any errors as a final step, regardless of whether there
is a destroy step explicitly written.
Create a new internal/provider/order_resource_test.go
file with the following.
internal/provider/order_resource_test.go
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccOrderResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "hashicups_order" "test" {
items = [
{
coffee = {
id = 1
}
quantity = 2
},
]
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify number of items
resource.TestCheckResourceAttr("hashicups_order.test", "items.#", "1"),
// Verify first order item
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.quantity", "2"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.id", "1"),
// Verify first coffee item has Computed attributes filled.
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.description", ""),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.image", "/hashicorp.png"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.name", "HCP Aeropress"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.price", "200"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.teaser", "Automation in a cup"),
// Verify dynamic values have any value set in the state.
resource.TestCheckResourceAttrSet("hashicups_order.test", "id"),
resource.TestCheckResourceAttrSet("hashicups_order.test", "last_updated"),
),
},
// ImportState testing
{
ResourceName: "hashicups_order.test",
ImportState: true,
ImportStateVerify: true,
// The last_updated attribute does not exist in the HashiCups
// API, therefore there is no value for it during import.
ImportStateVerifyIgnore: []string{"last_updated"},
},
// Update and Read testing
{
Config: providerConfig + `
resource "hashicups_order" "test" {
items = [
{
coffee = {
id = 2
}
quantity = 2
},
]
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify first order item updated
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.quantity", "2"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.id", "2"),
// Verify first coffee item has Computed attributes updated.
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.description", ""),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.image", "/packer.png"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.name", "Packer Spiced Latte"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.price", "350"),
resource.TestCheckResourceAttr("hashicups_order.test", "items.0.coffee.teaser", "Packed with goodness to spice up your images"),
),
},
// Delete testing automatically occurs in TestCase
},
})
}
Verify resource testing functionality
Now that you have implemented the testing functionality for the order resource, run the tests.
Run Go testing with the TF_ACC
environment variable set and only running the
resource tests. The test framework will report that your resource's test passed.
$ TF_ACC=1 go test -count=1 -run='TestAccOrderResource' -v
=== RUN TestAccOrderResource
--- PASS: TestAccOrderResource (2.01s)
PASS
ok terraform-provider-hashicups/internal/provider 2.754s
Implement function testing functionality
Function acceptance testing verifies that the function works as expected. Since provider functions require Terraform 1.8 or newer, the example code checks the version of Terraform before it runs the tests.
Create a new internal/provider/compute_tax_function_test.go
file with the
following.
internal/provider/compute_tax_function_test.go
package provider
import (
"regexp"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)
func TestComputeTaxFunction_Known(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_8_0),
},
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::hashicups::compute_tax(5.00, 0.085)
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckOutput("test", "5.43"),
),
},
},
})
}
func TestComputeTaxFunction_Null(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_8_0),
},
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::hashicups::compute_tax(null, 0.085)
}
`,
// The parameter does not enable AllowNullValue
ExpectError: regexp.MustCompile(`argument must not be null`),
},
},
})
}
Verify function testing functionality
Now that you have implemented the testing functionality for the compute_tax
function, run the tests.
Run Go testing with the TF_ACC
environment variable set and only running the
function tests. The test framework will report that your function's test passed.
$ TF_ACC=1 go test -count=1 -run='TestComputeTaxFunction' -v
=== RUN TestComputeTaxFunction_Known
--- PASS: TestComputeTaxFunction_Known (0.50s)
=== RUN TestComputeTaxFunction_Null
--- PASS: TestComputeTaxFunction_Null (0.13s)
PASS
ok terraform-provider-hashicups/internal/provider 1.070s
Navigate to the terraform-provider-hashicups
directory.
$ cd ../..
Next steps
Congratulations! You have enhanced the provider with acceptance testing capabilities.
If you were stuck during this tutorial, checkout the
10-acceptance-tests
directory in the example repository to see the code implemented in this
tutorial.
- To learn more about the Terraform Plugin Framework, refer to the Terraform Plugin Framework documentation.
- For a full capability comparison between the SDKv2 and the Plugin Framework, refer to the Which SDK Should I Use? documentation.
- The example repository contains directories corresponding to each tutorial in this collection.
- Submit any Terraform Plugin Framework bug reports or feature requests to the development team in the Terraform Plugin Framework Github repository.
- Submit any Terraform Plugin Framework questions in the Terraform Plugin Framework Discuss forum.