-
Notifications
You must be signed in to change notification settings - Fork 430
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
I was thinking for some time and finally had a moment to put all this in writing. This is a PoC of more readable and reusable assertions (resource and snowflake object ones) and better config builders for acceptance tests. Check the details in the README.
- Loading branch information
1 parent
5b63c62
commit ef496c2
Showing
27 changed files
with
1,949 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package assert | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
"github.com/hashicorp/terraform-plugin-testing/terraform" | ||
) | ||
|
||
// TestCheckFuncProvider is an interface with just one method providing resource.TestCheckFunc. | ||
// It allows using it as input the "Check:" in resource.TestStep. | ||
// It should be used with AssertThat. | ||
type TestCheckFuncProvider interface { | ||
ToTerraformTestCheckFunc(t *testing.T) resource.TestCheckFunc | ||
} | ||
|
||
// AssertThat should be used for "Check:" input in resource.TestStep instead of e.g. resource.ComposeTestCheckFunc. | ||
// It allows performing all the checks implementing the TestCheckFuncProvider interface. | ||
func AssertThat(t *testing.T, fs ...TestCheckFuncProvider) resource.TestCheckFunc { | ||
t.Helper() | ||
return func(s *terraform.State) error { | ||
var result []error | ||
|
||
for i, f := range fs { | ||
if err := f.ToTerraformTestCheckFunc(t)(s); err != nil { | ||
result = append(result, fmt.Errorf("check %d/%d error:\n%w", i+1, len(fs), err)) | ||
} | ||
} | ||
|
||
return errors.Join(result...) | ||
} | ||
} | ||
|
||
var _ TestCheckFuncProvider = (*testCheckFuncWrapper)(nil) | ||
|
||
type testCheckFuncWrapper struct { | ||
f resource.TestCheckFunc | ||
} | ||
|
||
func (w *testCheckFuncWrapper) ToTerraformTestCheckFunc(_ *testing.T) resource.TestCheckFunc { | ||
return w.f | ||
} | ||
|
||
// Check allows using the basic terraform checks while using AssertThat. | ||
// To use, just simply wrap the check in Check. | ||
func Check(f resource.TestCheckFunc) TestCheckFuncProvider { | ||
return &testCheckFuncWrapper{f} | ||
} | ||
|
||
// ImportStateCheckFuncProvider is an interface with just one method providing resource.ImportStateCheckFunc. | ||
// It allows using it as input the "ImportStateCheck:" in resource.TestStep for import tests. | ||
// It should be used with AssertThatImport. | ||
type ImportStateCheckFuncProvider interface { | ||
ToTerraformImportStateCheckFunc(t *testing.T) resource.ImportStateCheckFunc | ||
} | ||
|
||
// AssertThatImport should be used for "ImportStateCheck:" input in resource.TestStep instead of e.g. importchecks.ComposeImportStateCheck. | ||
// It allows performing all the checks implementing the ImportStateCheckFuncProvider interface. | ||
func AssertThatImport(t *testing.T, fs ...ImportStateCheckFuncProvider) resource.ImportStateCheckFunc { | ||
t.Helper() | ||
return func(s []*terraform.InstanceState) error { | ||
var result []error | ||
|
||
for i, f := range fs { | ||
if err := f.ToTerraformImportStateCheckFunc(t)(s); err != nil { | ||
result = append(result, fmt.Errorf("check %d/%d error:\n%w", i+1, len(fs), err)) | ||
} | ||
} | ||
|
||
return errors.Join(result...) | ||
} | ||
} | ||
|
||
var _ ImportStateCheckFuncProvider = (*importStateCheckFuncWrapper)(nil) | ||
|
||
type importStateCheckFuncWrapper struct { | ||
f resource.ImportStateCheckFunc | ||
} | ||
|
||
func (w *importStateCheckFuncWrapper) ToTerraformImportStateCheckFunc(_ *testing.T) resource.ImportStateCheckFunc { | ||
return w.f | ||
} | ||
|
||
// CheckImport allows using the basic terraform import checks while using AssertThatImport. | ||
// To use, just simply wrap the check in CheckImport. | ||
func CheckImport(f resource.ImportStateCheckFunc) ImportStateCheckFuncProvider { | ||
return &importStateCheckFuncWrapper{f} | ||
} | ||
|
||
// InPlaceAssertionVerifier is an interface providing a method allowing verifying all the prepared assertions in place. | ||
// It does not return function like TestCheckFuncProvider or ImportStateCheckFuncProvider; it runs all the assertions in place instead. | ||
type InPlaceAssertionVerifier interface { | ||
VerifyAll(t *testing.T) | ||
} | ||
|
||
// AssertThatObject should be used in the SDK tests for created object validation. | ||
// It verifies all the prepared assertions in place. | ||
func AssertThatObject(t *testing.T, objectAssert InPlaceAssertionVerifier) { | ||
t.Helper() | ||
objectAssert.VerifyAll(t) | ||
} |
133 changes: 133 additions & 0 deletions
133
pkg/acceptance/bettertestspoc/assert/resource_assertions.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package assert | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" | ||
"github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
"github.com/hashicorp/terraform-plugin-testing/terraform" | ||
) | ||
|
||
var ( | ||
_ TestCheckFuncProvider = (*ResourceAssert)(nil) | ||
_ ImportStateCheckFuncProvider = (*ResourceAssert)(nil) | ||
) | ||
|
||
// ResourceAssert is an embeddable struct that should be used to construct new resource assertions (for resource, show output, parameters, etc.). | ||
// It implements both TestCheckFuncProvider and ImportStateCheckFuncProvider which makes it easy to create new resource assertions. | ||
type ResourceAssert struct { | ||
name string | ||
id string | ||
prefix string | ||
assertions []resourceAssertion | ||
} | ||
|
||
// NewResourceAssert creates a ResourceAssert where the resource name should be used as a key for assertions. | ||
func NewResourceAssert(name string, prefix string) *ResourceAssert { | ||
return &ResourceAssert{ | ||
name: name, | ||
prefix: prefix, | ||
assertions: make([]resourceAssertion, 0), | ||
} | ||
} | ||
|
||
// NewImportedResourceAssert creates a ResourceAssert where the resource id should be used as a key for assertions. | ||
func NewImportedResourceAssert(id string, prefix string) *ResourceAssert { | ||
return &ResourceAssert{ | ||
id: id, | ||
prefix: prefix, | ||
assertions: make([]resourceAssertion, 0), | ||
} | ||
} | ||
|
||
type resourceAssertionType string | ||
|
||
const ( | ||
resourceAssertionTypeValueSet = "VALUE_SET" | ||
resourceAssertionTypeValueNotSet = "VALUE_NOT_SET" | ||
) | ||
|
||
type resourceAssertion struct { | ||
fieldName string | ||
expectedValue string | ||
resourceAssertionType resourceAssertionType | ||
} | ||
|
||
func valueSet(fieldName string, expected string) resourceAssertion { | ||
return resourceAssertion{fieldName: fieldName, expectedValue: expected, resourceAssertionType: resourceAssertionTypeValueSet} | ||
} | ||
|
||
func valueNotSet(fieldName string) resourceAssertion { | ||
return resourceAssertion{fieldName: fieldName, resourceAssertionType: resourceAssertionTypeValueNotSet} | ||
} | ||
|
||
const showOutputPrefix = "show_output.0." | ||
|
||
func showOutputValueSet(fieldName string, expected string) resourceAssertion { | ||
return resourceAssertion{fieldName: showOutputPrefix + fieldName, expectedValue: expected, resourceAssertionType: resourceAssertionTypeValueSet} | ||
} | ||
|
||
const ( | ||
parametersPrefix = "parameters.0." | ||
parametersValueSuffix = ".0.value" | ||
parametersLevelSuffix = ".0.level" | ||
) | ||
|
||
func parameterValueSet(fieldName string, expected string) resourceAssertion { | ||
return resourceAssertion{fieldName: parametersPrefix + fieldName + parametersValueSuffix, expectedValue: expected, resourceAssertionType: resourceAssertionTypeValueSet} | ||
} | ||
|
||
func parameterLevelSet(fieldName string, expected string) resourceAssertion { | ||
return resourceAssertion{fieldName: parametersPrefix + fieldName + parametersLevelSuffix, expectedValue: expected, resourceAssertionType: resourceAssertionTypeValueSet} | ||
} | ||
|
||
// ToTerraformTestCheckFunc implements TestCheckFuncProvider to allow easier creation of new resource assertions. | ||
// It goes through all the assertion accumulated earlier and gathers the results of the checks. | ||
func (r *ResourceAssert) ToTerraformTestCheckFunc(t *testing.T) resource.TestCheckFunc { | ||
t.Helper() | ||
return func(s *terraform.State) error { | ||
var result []error | ||
|
||
for i, a := range r.assertions { | ||
switch a.resourceAssertionType { | ||
case resourceAssertionTypeValueSet: | ||
if err := resource.TestCheckResourceAttr(r.name, a.fieldName, a.expectedValue)(s); err != nil { | ||
errCut, _ := strings.CutPrefix(err.Error(), fmt.Sprintf("%s: ", r.name)) | ||
result = append(result, fmt.Errorf("%s %s assertion [%d/%d]: failed with error: %s", r.name, r.prefix, i+1, len(r.assertions), errCut)) | ||
} | ||
case resourceAssertionTypeValueNotSet: | ||
if err := resource.TestCheckNoResourceAttr(r.name, a.fieldName)(s); err != nil { | ||
errCut, _ := strings.CutPrefix(err.Error(), fmt.Sprintf("%s: ", r.name)) | ||
result = append(result, fmt.Errorf("%s %s assertion [%d/%d]: failed with error: %s", r.name, r.prefix, i+1, len(r.assertions), errCut)) | ||
} | ||
} | ||
} | ||
|
||
return errors.Join(result...) | ||
} | ||
} | ||
|
||
// ToTerraformImportStateCheckFunc implements ImportStateCheckFuncProvider to allow easier creation of new resource assertions. | ||
// It goes through all the assertion accumulated earlier and gathers the results of the checks. | ||
func (r *ResourceAssert) ToTerraformImportStateCheckFunc(t *testing.T) resource.ImportStateCheckFunc { | ||
t.Helper() | ||
return func(s []*terraform.InstanceState) error { | ||
var result []error | ||
|
||
for i, a := range r.assertions { | ||
switch a.resourceAssertionType { | ||
case resourceAssertionTypeValueSet: | ||
if err := importchecks.TestCheckResourceAttrInstanceState(r.id, a.fieldName, a.expectedValue)(s); err != nil { | ||
result = append(result, fmt.Errorf("%s %s assertion [%d/%d]: failed with error: %w", r.id, r.prefix, i+1, len(r.assertions), err)) | ||
} | ||
case resourceAssertionTypeValueNotSet: | ||
panic("implement") | ||
} | ||
} | ||
|
||
return errors.Join(result...) | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
pkg/acceptance/bettertestspoc/assert/snowflake_assertions.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package assert | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" | ||
"github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
"github.com/hashicorp/terraform-plugin-testing/terraform" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type ( | ||
assertSdk[T any] func(*testing.T, T) error | ||
objectProvider[T any, I sdk.ObjectIdentifier] func(*testing.T, I) (*T, error) | ||
) | ||
|
||
// SnowflakeObjectAssert is an embeddable struct that should be used to construct new Snowflake object assertions. | ||
// It implements both TestCheckFuncProvider and ImportStateCheckFuncProvider which makes it easy to create new resource assertions. | ||
type SnowflakeObjectAssert[T any, I sdk.ObjectIdentifier] struct { | ||
assertions []assertSdk[*T] | ||
id I | ||
objectType sdk.ObjectType | ||
object *T | ||
provider objectProvider[T, I] | ||
} | ||
|
||
// NewSnowflakeObjectAssertWithProvider creates a SnowflakeObjectAssert with id and the provider. | ||
// Object to check is lazily fetched from Snowflake when the checks are being run. | ||
func NewSnowflakeObjectAssertWithProvider[T any, I sdk.ObjectIdentifier](objectType sdk.ObjectType, id I, provider objectProvider[T, I]) *SnowflakeObjectAssert[T, I] { | ||
return &SnowflakeObjectAssert[T, I]{ | ||
assertions: make([]assertSdk[*T], 0), | ||
id: id, | ||
objectType: objectType, | ||
provider: provider, | ||
} | ||
} | ||
|
||
// NewSnowflakeObjectAssertWithObject creates a SnowflakeObjectAssert with object that was already fetched from Snowflake. | ||
// All the checks are run against the given object. | ||
func NewSnowflakeObjectAssertWithObject[T any, I sdk.ObjectIdentifier](objectType sdk.ObjectType, id I, object *T) *SnowflakeObjectAssert[T, I] { | ||
return &SnowflakeObjectAssert[T, I]{ | ||
assertions: make([]assertSdk[*T], 0), | ||
id: id, | ||
objectType: objectType, | ||
object: object, | ||
} | ||
} | ||
|
||
// ToTerraformTestCheckFunc implements TestCheckFuncProvider to allow easier creation of new Snowflake object assertions. | ||
// It goes through all the assertion accumulated earlier and gathers the results of the checks. | ||
func (s *SnowflakeObjectAssert[_, _]) ToTerraformTestCheckFunc(t *testing.T) resource.TestCheckFunc { | ||
t.Helper() | ||
return func(_ *terraform.State) error { | ||
return s.runSnowflakeObjectsAssertions(t) | ||
} | ||
} | ||
|
||
// ToTerraformImportStateCheckFunc implements ImportStateCheckFuncProvider to allow easier creation of new Snowflake object assertions. | ||
// It goes through all the assertion accumulated earlier and gathers the results of the checks. | ||
func (s *SnowflakeObjectAssert[_, _]) ToTerraformImportStateCheckFunc(t *testing.T) resource.ImportStateCheckFunc { | ||
t.Helper() | ||
return func(_ []*terraform.InstanceState) error { | ||
return s.runSnowflakeObjectsAssertions(t) | ||
} | ||
} | ||
|
||
// VerifyAll implements InPlaceAssertionVerifier to allow easier creation of new Snowflake object assertions. | ||
// It verifies all the assertions accumulated earlier and gathers the results of the checks. | ||
func (s *SnowflakeObjectAssert[_, _]) VerifyAll(t *testing.T) { | ||
t.Helper() | ||
err := s.runSnowflakeObjectsAssertions(t) | ||
require.NoError(t, err) | ||
} | ||
|
||
func (s *SnowflakeObjectAssert[T, _]) runSnowflakeObjectsAssertions(t *testing.T) error { | ||
t.Helper() | ||
|
||
var sdkObject *T | ||
var err error | ||
switch { | ||
case s.object != nil: | ||
sdkObject = s.object | ||
case s.provider != nil: | ||
sdkObject, err = s.provider(t, s.id) | ||
if err != nil { | ||
return err | ||
} | ||
default: | ||
return fmt.Errorf("cannot proceed with object %s[%s] assertion: object or provider must be specified", s.objectType, s.id.FullyQualifiedName()) | ||
} | ||
|
||
var result []error | ||
|
||
for i, assertion := range s.assertions { | ||
if err = assertion(t, sdkObject); err != nil { | ||
result = append(result, fmt.Errorf("object %s[%s] assertion [%d/%d]: failed with error: %w", s.objectType, s.id.FullyQualifiedName(), i+1, len(s.assertions), err)) | ||
} | ||
} | ||
|
||
return errors.Join(result...) | ||
} |
Oops, something went wrong.