Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding resource.TestCheckResourceAttrWith test utility #950

Merged
merged 6 commits into from Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/950.txt
@@ -0,0 +1,3 @@
```release-note:feature
helper/resource: New `TestCheckResourceAttrWith` test helper, that allows for custom checking of attribute values.
detro marked this conversation as resolved.
Show resolved Hide resolved
```
33 changes: 31 additions & 2 deletions helper/resource/testing.go
Expand Up @@ -13,10 +13,11 @@ import (
"time"

"github.com/hashicorp/go-multierror"
testing "github.com/mitchellh/go-testing-interface"
"github.com/mitchellh/go-testing-interface"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/addrs"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
Expand Down Expand Up @@ -49,7 +50,7 @@ var flagSweepAllowFailures = flag.Bool("sweep-allow-failures", false, "Enable to
var flagSweepRun = flag.String("sweep-run", "", "Comma seperated list of Sweeper Tests to run")
var sweeperFuncs map[string]*Sweeper

// type SweeperFunc is a signature for a function that acts as a sweeper. It
// SweeperFunc is a signature for a function that acts as a sweeper. It
// accepts a string for the region that the sweeper is to be ran in. This
// function must be able to construct a valid client for that region.
type SweeperFunc func(r string) error
Expand Down Expand Up @@ -959,6 +960,34 @@ func testCheckResourceAttr(is *terraform.InstanceState, name string, key string,
return nil
}

// CheckResourceAttrWithFunc is the callback type used to apply a custom checking logic
// when using TestCheckResourceAttrWith and a value is found for the given name and key.
//
// When this function returns an error, TestCheckResourceAttrWith will fail the check.
type CheckResourceAttrWithFunc func(value string) error

// TestCheckResourceAttrWith - as per TestCheckResourceAttr but the attribute value
// checking logic can be customised.
//
// If a value is found for the given name and key, it is passed to the CheckResourceAttrWithFunc function.
// The CheckResourceAttrWithFunc function can then apply any checking logic:
// should return an error for the check to fail, or `nil` to succeed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this description here, I'm just wondering if we should also give provider developers all the usage information when they reference it in Go documentation or via the Go language server (e.g. hovering in an editor), similar to the other TestCheckFunc

// TestCheckResourceAttrWith ensures a specific value is stored in state for
// the given name and key combination, by checking it against a custom
// validation function. State value checking is only recommended for testing
// Computed attributes and attribute defaults.
//
// For managed resources, the name parameter is combination of the resource
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
//     resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
//     data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the following special key syntax to inspect list, map, and
// set attributes:
//
//     - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
//        Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
//        for sets.
//     - .{KEY}: Map value at key, e.g. .example to inspect the example key
//        value.
//
// The f parameter is a function containing the custom value validation logic.
// Any returned errors from that function will cause the check and test to fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I'll update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have provided a much more comprehensive godoc for this function.

Let me know your thoughts

func TestCheckResourceAttrWith(name, key string, f CheckResourceAttrWithFunc) TestCheckFunc {
return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error {
is, err := primaryInstanceState(s, name)
if err != nil {
return err
}

err = testCheckResourceAttrSet(is, name, key)
if err != nil {
return err
}

return f(is.Attributes[key])
detro marked this conversation as resolved.
Show resolved Hide resolved
})
}

// TestCheckNoResourceAttr ensures no value exists in the state for the
// given name and key combination. The opposite of this TestCheckFunc is
// TestCheckResourceAttrSet. State value checking is only recommended for
Expand Down
63 changes: 63 additions & 0 deletions helper/resource/testing_example_test.go
@@ -1,7 +1,9 @@
package resource_test

import (
"fmt"
"regexp"
"strconv"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)
Expand Down Expand Up @@ -209,6 +211,67 @@ func ExampleTestCheckResourceAttr_typeString() {
resource.TestCheckResourceAttr("example_thing.test", "example_string_attribute", "test-value")
}

func ExampleTestCheckResourceAttrWith_typeString() {
// This function is typically implemented in a TestStep type Check field,
// wrapped with ComposeAggregateTestCheckFunc to combine results from
// multiple checks.
//
// Given the following example configuration:
//
// resource "example_thing" "test" {
// example_string_attribute = "Very long string..."
// }
//
// The following TestCheckResourceAttrWith can be written to assert against
// the expected state values.
//
// NOTE: State value checking is only necessary for Computed attributes,
// as the testing framework will automatically return test failures
// for configured attributes that mismatch the saved state, however
// this configuration and test is shown for illustrative purposes.

// Verify the attribute value string length is above 1000
resource.TestCheckResourceAttrWith("example_thing.test", "example_string_attribute", func(value string) error {
if len(value) <= 1000 {
return fmt.Errorf("example_string_attribute should be longer than 1000 characters")
detro marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
})
}

func ExampleTestCheckResourceAttrWith_typeInt() {
// This function is typically implemented in a TestStep type Check field,
// wrapped with ComposeAggregateTestCheckFunc to combine results from
// multiple checks.
//
// Given the following example configuration:
//
// resource "example_thing" "test" {
// example_int_attribute = 10
// }
//
// The following TestCheckResourceAttrWith can be written to assert against
// the expected state values.
//
// NOTE: State value checking is only necessary for Computed attributes,
// as the testing framework will automatically return test failures
// for configured attributes that mismatch the saved state, however
// this configuration and test is shown for illustrative purposes.

// Verify the attribute value is an integer, and it's between 5 (included) and 20 (excluded)
resource.TestCheckResourceAttrWith("example_thing.test", "example_string_attribute", func(value string) error {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the second parameter be example_int_attribute in this case?

valueInt, err := strconv.Atoi(value)
if err != nil {
return err
}

if valueInt < 5 && valueInt >= 20 {
return fmt.Errorf("example error message")
detro marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
})
}

func ExampleTestCheckResourceAttrPair() {
// This function is typically implemented in a TestStep type Check field,
// wrapped with ComposeAggregateTestCheckFunc to combine results from
Expand Down