Skip to content

Commit

Permalink
feat: Better tests poc (#2917)
Browse files Browse the repository at this point in the history
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
sfc-gh-asawicki authored Jul 11, 2024
1 parent 5b63c62 commit ef496c2
Show file tree
Hide file tree
Showing 27 changed files with 1,949 additions and 146 deletions.
1 change: 0 additions & 1 deletion docs/resources/table_constraint.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,5 @@ Required:
Import is supported using the following syntax:

```shell
# format is constraint name ❄ constraint type ❄ database name | schema name | table name
terraform import snowflake_table_constraint.example 'myconstraintfk❄️FOREIGN KEY❄️databaseName|schemaName|tableName'
```
470 changes: 470 additions & 0 deletions pkg/acceptance/bettertestspoc/README.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions pkg/acceptance/bettertestspoc/assert/commons.go
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 pkg/acceptance/bettertestspoc/assert/resource_assertions.go
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 pkg/acceptance/bettertestspoc/assert/snowflake_assertions.go
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...)
}
Loading

0 comments on commit ef496c2

Please sign in to comment.