Unit testing

As pointed out in this AWS blog post by Jason Del Ponte:

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.

In this section you will modify your code to save the request body to an Amazon DynamoDB table using the DynamoDB Interface and create a service mock for testing your function via dependency injection.

SAM SimpleTable

In order to save your requests to DynamoDB, you first need to create a table. SAM allows you to do this in only a few lines using the AWS::Serverless::SimpleTable resource.

Copy and paste the following text into your template.yaml file in the Resources section:

Resources:
  ...
  VoteTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      TableName: votes
      PrimaryKey:
        Name: ID
        Type: String

Run sam deploy:

sam deploy

This creates a single DynamoDB table named votes with a primary key ID of type string. You can confirm this by visiting the DynamoDB page in the AWS Management Console or by running the following command in a terminal:

aws dynamodb list-tables

Dependency injection

Examine the following code excerpt to see how dependency injection is implemented in Go.

main.go (excerpt)

type dependency struct {
	  ddb   dynamodbiface.DynamoDBAPI
	  table string
}

func (d *dependency) LambdaHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // Check for dependencies, e.g., test injections.
    // If not present, create them with a live session.
    if d.ddb == nil {
        sess := session.Must(session.NewSession())
        svc := dynamodb.New(sess)

        d = &dependency{
            ddb:   svc,
            table: os.Getenv("DYNAMODB_TABLE"),
        }
    }

    // Business logic

    // Error handling and return
}

func main() {
	d := dependency{}

	lambda.Start(d.LambdaHandler)
}
  • In lines 1-4, you declare a receiver type dependency that includes a DynamoDB Interface (not client) and a string representing the table name.
  • On line 6, you modify the Lambda function handler to depend on the receiver type dependency.
  • On line 25, in your main function, you create an empty dependency. In execution, this will be replaced with an instance of a DynamoDB client in lines nine through seventeen. In testing, it will be replaced by the mock you pass in.
  • On line 27, you modify the lambda.Start() call to invoke your new function via the receiving dependency.

main_test.go (excerpt)

type mockedPutItem struct {
	dynamodbiface.DynamoDBAPI
	Response dynamodb.PutItemOutput
}

func (d mockedPutItem) PutItem(in *dynamodb.PutItemInput) (*dynamodb.PutItemOutput, error) {
	return &d.Response, nil
}

func TestLambdaHandler(t *testing.T) {
	t.Run("Successful Request", func(t *testing.T) {

		m := mockedPutItem{
			Response: dynamodb.PutItemOutput{},
		}

		d := dependency{
			ddb:   m,
			table: "test_table",
		}

		_, err := d.LambdaHandler(events.APIGatewayProxyRequest{})
		if err != nil {
			t.Fatal("Everything should be ok")
		}
	})
}
  • In lines 1-4, you declare a type to implement your mocked DynamoDB service client.
  • In lines 6-8, you define the behavior of your mock for the PutItem() call.
  • In lines 17-20, you create a dependency instance that relies on your mock.
  • On line 22, you call the Lambda handler using your mocked dependency.

Modify the function

Now that you have a table, modify your function to save the body of each request into DynamoDB. Basic requirements:

  • Use dependency injection so that your function can be tested using a mocked DynamoDB client.

  • Use the following Record data type, where:

    • ID is the request ID (request.RequestContext.RequestID)
    • Body is the request body (request.Body)
// Record represents one record in the DynamoDB table
type Record struct {
	ID   string
	Body string
}
  • If an error occurs while executing your business logic, stop processing and return the error. Otherwise return an events.APIGatewayProxyResponse object with a StatusCode of 200.
Hint

Try to write the functions on your own using the documentation and examples linked in the menu. If you get stuck, expand the sample solution below.

template.yaml

Click to expand

main.go

Click to expand

main_test.go

Click to expand

Running your function

If you build and run your function at this point it probably isn't going to work. That's okay! Over the next few sections we'll debug our application, both locally and in the cloud.