Skip to content

Commit e1682fb

Browse files
Unit Testing Framework w/ Feature Group sample (#66)
Summary: Added unit testing framework (and code coverage), with an example of a unit test in feature group. Code Coverage: 1.8% Files Added: - test/mocks/aws-sdk-go/sagemaker/SageMakerAPI.go - Contains unit testing api mocks. - pkg/resource/feature_group/manager_test_suite_test.go - Contains resource specific helper functions for unit testing, and the provideResourceManagerWithMockSDKAPI function for unit testing. - pkg/resource/feature_group/testdata/feature_group/v1alpha1/fg_invalid_before_create.yaml - Input sample file for unit testing. - pkg/resource/feature_group/testdata/feature_group/v1alpha1/fg_invalid_create_attempted.yaml - Output (expected) sample file for unit testing. - pkg/resource/feature_group/testdata/test_suite.yaml - Contains unit testing scenarios (which input/output to test along with the functions (Create/ReadOne/Update/Delete) to test). - pkg/testutil/test_suite_config.go - Defines structures related to the unit testing test suite. - pkg/testutil/test_suite_runner.go - Contains main workflow functions for running unit tests. - pkg/testutil/util.go - Contains helper functions used in test_suite_runner.go. - test/scripts/install-mockery.sh - A script that installs the mockery CLI tool that is used to build Go mocks for our interfaces to use in unit testing. Files Edited: - Makefile - Defined unit testing, mock, and unit testing coverage related testing calls.
1 parent 871401b commit e1682fb

File tree

13 files changed

+20928
-1
lines changed

13 files changed

+20928
-1
lines changed

ATTRIBUTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ License version 2.0, we include the full text of the package's License below.
3232
* `github.com/spf13/pflag`
3333
* `github.com/stretchr/testify`
3434
* `golang.org/x/mod`
35+
* `go.uber.org/zap`
3536
* `k8s.io/api`
3637
* `k8s.io/apimachinery`
3738
* `k8s.io/client-go`

Makefile

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ SHELL := /bin/bash # Use bash syntax
33
# Set up variables
44
GO111MODULE=on
55

6+
AWS_SDK_GO_VERSION="$(shell echo $(shell go list -m -f '{{.Version}}' github.com/aws/aws-sdk-go))"
7+
AWS_SDK_GO_VERSIONED_PATH="$(shell echo github.com/aws/aws-sdk-go@$(AWS_SDK_GO_VERSION))"
8+
SAGEMAKER_API_PATH="$(shell echo $(shell go env GOPATH))/pkg/mod/$(AWS_SDK_GO_VERSIONED_PATH)/service/sagemaker/sagemakeriface"
9+
SERVICE_CONTROLLER_SRC_PATH="$(shell pwd)"
10+
611
# Build ldflags
712
VERSION ?= "v0.0.0"
813
GITCOMMIT=$(shell git rev-parse HEAD)
@@ -11,13 +16,32 @@ GO_LDFLAGS=-ldflags "-X main.version=$(VERSION) \
1116
-X main.buildHash=$(GITCOMMIT) \
1217
-X main.buildDate=$(BUILDDATE)"
1318

14-
.PHONY: all test
19+
.PHONY: all test clean-mocks mocks
1520

1621
all: test
1722

1823
test: ## Run code tests
1924
go test -v ./...
2025

26+
test-cover: | mocks ## Run code tests with resources coverage
27+
go test -coverpkg=./pkg/resource/... -covermode=count -coverprofile=coverage.out ./...
28+
go tool cover -func=coverage.out
29+
30+
clean-mocks: ## Remove mocks directory
31+
rm -r mocks
32+
33+
install-mockery:
34+
@test/scripts/install-mockery.sh
35+
36+
mocks: install-mockery ## Build mocks
37+
go get -d $(AWS_SDK_GO_VERSIONED_PATH)
38+
@echo "building mocks for $(SAGEMAKER_API_PATH) ... "
39+
@pushd $(SAGEMAKER_API_PATH) 1>/dev/null; \
40+
$(SERVICE_CONTROLLER_SRC_PATH)/bin/mockery --all --dir=. --output=$(SERVICE_CONTROLLER_SRC_PATH)/mocks/aws-sdk-go/sagemaker/ ; \
41+
popd 1>/dev/null;
42+
@echo "ok."
43+
44+
2145
help: ## Show this help.
2246
@grep -F -h "##" $(MAKEFILE_LIST) | grep -F -v grep | sed -e 's/\\$$//' \
2347
| awk -F'[:#]' '{print $$1 = sprintf("%-30s", $$1), $$4}'

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ go 1.14
55
require (
66
github.com/aws-controllers-k8s/runtime v0.6.0
77
github.com/aws/aws-sdk-go v1.38.11
8+
github.com/ghodss/yaml v1.0.0
89
github.com/go-logr/logr v0.1.0
10+
github.com/google/go-cmp v0.3.1
911
github.com/spf13/pflag v1.0.5
12+
github.com/stretchr/testify v1.5.1
13+
go.uber.org/zap v1.10.0
1014
k8s.io/api v0.18.2
1115
k8s.io/apimachinery v0.18.6
1216
k8s.io/client-go v0.18.2

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
7171
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
7272
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
7373
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
74+
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
7475
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
7576
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
7677
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package feature_group
15+
16+
import (
17+
"errors"
18+
"fmt"
19+
mocksvcsdkapi "github.com/aws-controllers-k8s/sagemaker-controller/test/mocks/aws-sdk-go/sagemaker"
20+
"github.com/aws-controllers-k8s/sagemaker-controller/pkg/testutil"
21+
acktypes "github.com/aws-controllers-k8s/runtime/pkg/types"
22+
svcsdk "github.com/aws/aws-sdk-go/service/sagemaker"
23+
"github.com/google/go-cmp/cmp"
24+
"github.com/google/go-cmp/cmp/cmpopts"
25+
"path/filepath"
26+
"testing"
27+
ctrlrtzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
28+
ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics"
29+
"go.uber.org/zap/zapcore"
30+
)
31+
32+
// provideResourceManagerWithMockSDKAPI accepts MockSageMakerAPI and returns pointer to resourceManager
33+
// the returned resourceManager is configured to use mockapi api.
34+
func provideResourceManagerWithMockSDKAPI(mockSageMakerAPI *mocksvcsdkapi.SageMakerAPI) *resourceManager {
35+
zapOptions := ctrlrtzap.Options{
36+
Development: true,
37+
Level: zapcore.InfoLevel,
38+
}
39+
fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions))
40+
return &resourceManager{
41+
rr: nil,
42+
awsAccountID: "",
43+
awsRegion: "",
44+
sess: nil,
45+
sdkapi: mockSageMakerAPI,
46+
log: fakeLogger,
47+
metrics: ackmetrics.NewMetrics("sagemaker"),
48+
}
49+
}
50+
51+
// TestDeclarativeTestSuite runs the test suite for feature group
52+
func TestDeclarativeTestSuite(t *testing.T) {
53+
defer func() {
54+
if r := recover(); r != nil {
55+
fmt.Println(testutil.RecoverPanicString, r)
56+
t.Fail()
57+
}
58+
}()
59+
var ts = testutil.TestSuite{}
60+
testutil.LoadFromFixture(filepath.Join("testdata", "test_suite.yaml"), &ts)
61+
var delegate = testRunnerDelegate{t: t}
62+
var runner = testutil.TestSuiteRunner{TestSuite: &ts, Delegate: &delegate}
63+
runner.RunTests()
64+
}
65+
66+
// testRunnerDelegate implements testutil.TestRunnerDelegate
67+
type testRunnerDelegate struct {
68+
t *testing.T
69+
}
70+
71+
func (d *testRunnerDelegate) ResourceDescriptor() acktypes.AWSResourceDescriptor {
72+
return &resourceDescriptor{}
73+
}
74+
75+
func (d *testRunnerDelegate) ResourceManager(mocksdkapi *mocksvcsdkapi.SageMakerAPI) acktypes.AWSResourceManager {
76+
return provideResourceManagerWithMockSDKAPI(mocksdkapi)
77+
}
78+
79+
func (d *testRunnerDelegate) GoTestRunner() *testing.T {
80+
return d.t
81+
}
82+
83+
func (d *testRunnerDelegate) EmptyServiceAPIOutput(apiName string) (interface{}, error) {
84+
if apiName == "" {
85+
return nil, errors.New("no API name specified")
86+
}
87+
//TODO: use reflection, template to auto generate this block/method.
88+
switch apiName {
89+
case "CreateFeatureGroupWithContext":
90+
var output svcsdk.CreateFeatureGroupOutput
91+
return &output, nil
92+
case "DeleteFeatureGroupWithContext":
93+
var output svcsdk.DeleteFeatureGroupOutput
94+
return &output, nil
95+
case "DescribeFeatureGroupWithContext":
96+
var output svcsdk.DescribeFeatureGroupOutput
97+
return &output, nil
98+
}
99+
return nil, errors.New(fmt.Sprintf("no matching API name found for: %s", apiName))
100+
}
101+
102+
func (d *testRunnerDelegate) Equal(a acktypes.AWSResource, b acktypes.AWSResource) bool {
103+
ac := a.(*resource)
104+
bc := b.(*resource)
105+
opts := []cmp.Option{cmpopts.EquateEmpty()}
106+
107+
if cmp.Equal(ac.ko.Status, bc.ko.Status, opts...) {
108+
return true
109+
} else {
110+
fmt.Printf("Difference (-expected +actual):\n\n")
111+
fmt.Println(cmp.Diff(ac.ko.Status, bc.ko.Status, opts...))
112+
return false
113+
}
114+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: sagemaker.services.k8s.aws/v1alpha1
2+
kind: FeatureGroup
3+
metadata:
4+
name: unit-testing-feature-group
5+
spec:
6+
eventTimeFeatureName: EventTime
7+
featureDefinitions:
8+
- featureName: TransactionID
9+
featureType: Integral
10+
- featureName: EventTime
11+
featureType: Fractional
12+
featureGroupName: !-intentionally-invalid-name
13+
offlineStoreConfig:
14+
s3StorageConfig:
15+
s3URI: s3://source-data-bucket-592697580195-us-west-2/sagemaker/feature-group-data
16+
recordIdentifierFeatureName: TransactionID
17+
roleARN: arn:aws:iam::123456789012:role/ack-sagemaker-execution-role
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
apiVersion: sagemaker.services.k8s.aws/v1alpha1
2+
kind: FeatureGroup
3+
metadata:
4+
creationTimestamp: null
5+
name: unit-testing-feature-group
6+
spec:
7+
eventTimeFeatureName: EventTime
8+
featureDefinitions:
9+
- featureName: TransactionID
10+
featureType: Integral
11+
- featureName: EventTime
12+
featureType: Fractional
13+
featureGroupName: ""
14+
offlineStoreConfig:
15+
s3StorageConfig:
16+
s3URI: s3://source-data-bucket-592697580195-us-west-2/sagemaker/feature-group-data
17+
recordIdentifierFeatureName: TransactionID
18+
roleARN: arn:aws:iam::123456789012:role/ack-sagemaker-execution-role
19+
status:
20+
ackResourceMetadata:
21+
ownerAccountID: ""
22+
conditions:
23+
- message: The feature group name must start with an alphanumeric character.
24+
status: "True"
25+
type: ACK.Terminal
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
tests:
2+
- name: "Feature group create test."
3+
description: "Feature group CRD tests"
4+
scenarios:
5+
- name: "Create=InvalidInput"
6+
description: "Given one of the parameters is invalid, ko.Status shows a terminal condition"
7+
given:
8+
desired_state: "feature_group/v1alpha1/fg_invalid_before_create.yaml"
9+
svc_api:
10+
- operation: CreateFeatureGroupWithContext
11+
error:
12+
code: InvalidParameterValue
13+
message: "The feature group name must start with an alphanumeric character."
14+
invoke: Create
15+
expect:
16+
latest_state: "feature_group/v1alpha1/fg_invalid_create_attempted.yaml"
17+
error: resource is in terminal condition

pkg/testutil/test_suite_config.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package testutil
15+
16+
// TestSuite represents instructions to run unit tests using test fixtures and mock service apis
17+
type TestSuite struct {
18+
Tests []TestConfig `json:"tests"`
19+
}
20+
21+
// TestConfig represents declarative unit test
22+
type TestConfig struct {
23+
Name string `json:"name"`
24+
Description string `json:"description"`
25+
Scenarios []TestScenario `json:"scenarios"`
26+
}
27+
28+
// TestScenario represents declarative test scenario details
29+
type TestScenario struct {
30+
Name string `json:"name"`
31+
Description string `json:"description"`
32+
// Fixture lets you specify test scenario given input fixtures
33+
Fixture Fixture `json:"given"`
34+
// UnitUnderTest lets you specify the unit to test
35+
// For example resource manager API: ReadOne, Create, Update, Delete
36+
UnitUnderTest string `json:"invoke"`
37+
// Expect lets you specify test scenario expected outcome fixtures
38+
Expect Expect `json:"expect"`
39+
}
40+
41+
// Fixture represents test scenario fixture to load from file paths
42+
type Fixture struct {
43+
// DesiredState lets you specify fixture path to load the desired state fixture
44+
DesiredState string `json:"desired_state"`
45+
// LatestState lets you specify fixture path to load the current state fixture
46+
LatestState string `json:"latest_state"`
47+
// ServiceAPIs lets you specify fixture path to mock service sdk api response
48+
ServiceAPIs []ServiceAPI `json:"svc_api"`
49+
}
50+
51+
// ServiceAPI represents details about the the service sdk api and fixture path to mock its response
52+
type ServiceAPI struct {
53+
Operation string `json:"operation"`
54+
Output string `json:"output_fixture"`
55+
ServiceAPIError *ServiceAPIError `json:"error,omitempty"`
56+
}
57+
58+
// ServiceAPIError contains the specification for the error of the mock API response
59+
type ServiceAPIError struct {
60+
// Code here is usually the type of fault/error, not the HTTP status code
61+
Code string `json:"code"`
62+
Message string `json:"message"`
63+
}
64+
65+
// Expect represents test scenario expected outcome fixture to load from file path
66+
type Expect struct {
67+
LatestState string `json:"latest_state"`
68+
// Error is a string matching the message of the expected error returned from the ResourceManager operation.
69+
// Possible errors can be found in runtime/pkg/errors/error.go
70+
Error string `json:"error"`
71+
}

0 commit comments

Comments
 (0)