AWS Developer Blog

Mocking Out the AWS SDK for Go for Unit Testing

In our previous post, we showed how you could use the request handler stack in the AWS SDK for Go to extend or modify how requests are sent and received. Now, we’d like to expand the idea of extending the SDK and discuss how you can unit test code that uses the SDK. The SDK’s service clients are a common component to short circuit with custom unit test functionality.

You can easily mock out the SDK service clients by taking advantage of Go’s interfaces. By using the methods of an interface, your code can use that interface instead of using the concrete service client directly. This enables you to mock out the implementation of the service client for your unit tests. You can define these interfaces yourself or use the interfaces that the SDK already defines for each service client’s API. The service client’s API interfaces are very simple to use, and give you the flexibility to test your code. This pattern of using the API interfaces for mocking out is simple to use for new or existing code.

You can find the API interface package nested under each service client package, named “iface“. For example, the Amazon SQS API interface package is “sqsiface“, with the import path of github.com/aws/aws-sdk-go/service/sqs/sqsiface.

The following example shows one pattern how your code can use the SDK’s SQS API interface instead of the concrete client. We’ll take a look at how you could mock out the SDK’s SQS client next.

func main() {
	sess := session.Must(session.NewSession())

	q := Queue{
		Client: sqs.New(sess),
		URL:    os.Args[1],
	}

	msgs, err := q.GetMessages(20)
	// ...
}

type Queue struct {
	Client sqsiface.SQSAPI
	URL    string
}

func (q *Queue) GetMessages(waitTimeout int64) ([]Message, error) {
	params := sqs.ReceiveMessageInput{
		QueueUrl: aws.String(q.URL),
	}
	if waitTimeout > 0 {
		params.WaitTimeSeconds = aws.Int64(waitTimeout)
	}
	resp, err := q.Client.ReceiveMessage(&params)
	// ...
}

Because the previous code uses the SQS API interface, our tests can mock out the client with the responses we want so that we can verify GetMessages returns the parsed messages correctly.

type mockedReceiveMsgs struct {
	sqsiface.SQSAPI
	Resp sqs.ReceiveMessageOutput
}

func (m mockedReceiveMsgs) ReceiveMessage(in *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
	// Only need to return mocked response output
	return &m.Resp, nil
}

func TestQueueGetMessage(t *testing.T) {
	cases := []struct {
		Resp     sqs.ReceiveMessageOutput
		Expected []Message
	}{
		{
			Resp: sqs.ReceiveMessageOutput{
				Messages: []*sqs.Message{
					{Body: aws.String(`{"from":"user_1","to":"room_1","msg":"Hello!"}`)},
					{Body: aws.String(`{"from":"user_2","to":"room_1","msg":"Hi user_1 :)"}`)},
				},
			},
			Expected: []Message{
				{From: "user_1", To: "room_1", Msg: "Hello!"},
				{From: "user_2", To: "room_1", Msg: "Hi user_1 :)"},
			},
		},
	}

	for i, c := range cases {
		q := Queue{
			Client: mockedReceiveMsgs{Resp: c.Resp},
			URL:    fmt.Sprintf("mockURL_%d", i),
		}
		msgs, err := q.GetMessages(20)
		if err != nil {
			t.Fatalf("%d, unexpected error", err)
		}
		if a, e := len(msgs), len(c.Expected); a != e {
			t.Fatalf("%d, expected %d messages, got %d", i, e, a)
		}
		for j, msg := range msgs {
			if a, e := msg, c.Expected[j]; a != e {
				t.Errorf("%d, expected %v message, got %v", i, e, a)
			}
		}
	}
}

Using this pattern will help ensure your code that uses the SDK is easy to test and maintain. You can find the full code example used in this post on GitHub at https://github.com/aws/aws-sdk-go/tree/master/example/service/sqs/mockingClientsForTests.

TAGS: