Using reflection

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.

Usage Walkthrough

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:

Test Driven Development

These are the development steps for TDD I will follow:

flowchart LR id1((Start)) --> A A(Write empty Function) --> B(Write Test) classDef someclass fill:#f96; B --> T{Test}; U(Update Function) --> T T -- FAIL --> U; T -- PASS --> E((End));

1 Write empty function

package reflection

import ( "github.com/aws/aws-sdk-go-v2/service/ssm")

var Client *ssm.Client

	
func GetTableName(client *ssm.Client) *string {
	return nil
}

2 Write test that fails

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.

Create a mock function

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.

Wire the mock

  1. Create a Mock Handler

    mockCfg := awsmock.NewAwsMockHandler()

  2. Add the function to the handler

    mockCfg.AddHandler(GetParameterFunc)

  3. Create mock client

    client := ssm.NewFromConfig(mockCfg.AwsConfig())

Call the function

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…

3 Implement Function until test PASS

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.

Complete Code

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 also

Source

See the full source on github.

Sources