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

feat: Better tests poc #2917

Merged
merged 18 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
460 changes: 460 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
Loading