The default way of mocking AWS operations is to define an interface and let the test implement the interface. These are described in See also.
A simpler way is to let a handler decide, which operation is to use. So instead of explicitly defining an interface, I let reflection determine which API operation is mocked.
The example is: Reading a Systems Manager Parameter from the Parameter Store.
The SSM service is defined in "github.com/aws/aws-sdk-go-v2/service/ssm"
.
The GetParameter function is defined as:
func (*ssm.Client).GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
This is the function declaration, I have to implement as a mock.
Now I want to develop a function GetTableName to implement the example, which reads a name from the store:
func GetTableName(client *ssm.Client) *string
This comes in handy if you work with dynamically generated table names.
The name of the parameter (which holds the name of the table) is hardcoded as /go-on-aws/table
Quite simple:
These are the development steps for TDD I will follow:
package reflection
import ( "github.com/aws/aws-sdk-go-v2/service/ssm")
var Client *ssm.Client
func GetTableName(client *ssm.Client) *string {
return nil
}
I define a test, which compares the parameter value against the test value “anothertotalfancyname”:
package reflection_test
import "reflection"
func TestGetTableNameStruct(t *testing.T) {
name := reflection.GetTableName(client)
assert.Equal(t, "anothertotalfancyname",*name)
}
The go base module is named “reflection”. I use the package name reflection_test in this test. That way test code will not be built into the running code. But I have to import the module reflection first.
The first run of the test will give a pointer error as expected.
go test -run TestGetTableNameStruct
--- FAIL: TestGetTableNameStruct (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
...
Now I use the package "github.com/megaproaktiv/awsmock"
which provides the reflection capabilities.
This is the mock function, which will return my test values:
GetParameterFunc := func(ctx context.Context, params *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) {
out := &ssm.GetParameterOutput{
Parameter: &types.Parameter{
Value: aws.String("anothertotalfancyname"),
},
}
return out,nil
}
The function have to use the same declaration as the AWS SDK function GetParameter.
It defines the same input and output as ssm.GetParameter, but the optional optFns is left out. The GetParameterOutput is populated with the test return values.
The next step is to replace the real API call with this GetParameterFunc.
Create a Mock Handler
mockCfg := awsmock.NewAwsMockHandler()
Add the function to the handler
mockCfg.AddHandler(GetParameterFunc)
Create mock client
client := ssm.NewFromConfig(mockCfg.AwsConfig())
Now I call the tested function GetTableName with the mock client:
name := reflection.GetTableName(client)
The whole test is now:
func TestGetTableNameStruct(t *testing.T) {
GetParameterFunc := func(ctx context.Context, params *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) {
out := &ssm.GetParameterOutput{
Parameter: &types.Parameter{
Value: aws.String("anothertotalfancyname"),
},
}
return out,nil
}
// Create a Mock Handler
mockCfg := awsmock.NewAwsMockHandler()
// add a function to the handler
// Routing per paramater types
mockCfg.AddHandler(GetParameterFunc)
// Create mocking client
client := ssm.NewFromConfig(mockCfg.AwsConfig())
name := reflection.GetTableName(client)
assert.Equal(t, "anothertotalfancyname",*name)
}
This is a very simple version, you could test the GetParameterInput for values etc…
The function is now implemented:
func GetTableName(client *ssm.Client) *string {
parms := &ssm.GetParameterInput{
Name: aws.String("/go-on-aws/table"),
}
resp, err := client.GetParameter(context.TODO(), parms)
if err != nil {
panic("ssm error, " + err.Error())
}
value := resp.Parameter.Value
return value
}
Then the test run: go test -run TestGetTableNameStruct -v
gives PASS:
go test -run TestGetTableNameStruct -v
=== RUN TestGetTableNameStruct
--- PASS: TestGetTableNameStruct (0.00s)
PASS
ok reflection 0.132s
The source gives you the full example.
This way we do not have to declare interfaces for each function and writing mockup test becomes much easier.
See Chapter client SDKV2 to add the real SSM client.
In the next chapter I show you how to use json files on (almost) any API calls as input data.
package reflection
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ssm"
)
var Client *ssm.Client
func init(){
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
Client = ssm.NewFromConfig(cfg)
}
func GetTableName(client *ssm.Client) *string {
parms := &ssm.GetParameterInput{
Name: aws.String("/go-on-aws/table"),
}
resp, err := client.GetParameter(context.TODO(), parms)
if err != nil {
panic("ssm error, " + err.Error())
}
value := resp.Parameter.Value
return value
}
package reflection_test
import (
"context"
"fmt"
"reflection"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/megaproaktiv/awsmock"
"gotest.tools/assert"
)
func TestGetTableNameStruct(t *testing.T) {
GetParameterFunc := func(ctx context.Context, params *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) {
out := &ssm.GetParameterOutput{
Parameter: &types.Parameter{
Value: aws.String("anothertotalfancyname"),
},
}
return out,nil
}
mockCfg := awsmock.NewAwsMockHandler()
mockCfg.AddHandler(GetParameterFunc)
client := ssm.NewFromConfig(mockCfg.AwsConfig())
name := reflection.GetTableName(client)
assert.Equal(t, "anothertotalfancyname",*name)
}
See the full source on github.