Mock AWS API Gateway request and DynamoDB for Golang Lambda function unit Test

Viewed 2939

Setup

  • Windows 10
  • go v1.10.3
  • aws cli v1.16.67

What I'm trying to do

Test an AWS Lambda function written using golang. The function accepts a request from the API Gateway and then does some stuff with DynamoDB. Most of the below has been taken from this article (I'm a newbie with Go)

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "regexp"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

var uuidRegexp = regexp.MustCompile(`\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b`)
var errorLogger = log.New(os.Stderr, "ERROR ", log.Llongfile)

type job struct {
    ID                string `json:"id"`
    ClientID          string `json:"clientId"`
    Title             string `json:"title"`
    Count             int    `json:"count"`
}

// CreateJobCommand manages interactions with DynamoDB
func CreateJobCommand(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

    if req.Headers["Content-Type"] != "application/json" {
        return clientError(http.StatusNotAcceptable) //406
    }

    newJob := new(job)
    err := json.Unmarshal([]byte(req.Body), newJob)

    // Ensure request has deserialized correctly
    if err != nil {
        return clientError(http.StatusUnprocessableEntity) //422
    }

    // Validate ID and ClientID attributes match RegEx pattern
    if !uuidRegexp.MatchString(newJob.ID) || !uuidRegexp.MatchString(newJob.ClientID) {
        return clientError(http.StatusBadRequest)
    }

    // Mandatory field check
    if newJob.Title == "" {
        return clientError(http.StatusBadRequest)
    }

    // Put item in database
    err = putItem(newJob) // putItem is defined in another file
    if err != nil {
        return serverError(err)
    }

    return events.APIGatewayProxyResponse{
        StatusCode: 201,
    }, nil
}

// Add a helper for handling errors. This logs any error to os.Stderr
// and returns a 500 Internal Server Error response that the AWS API
// Gateway understands.
func serverError(err error) (events.APIGatewayProxyResponse, error) {
    errorLogger.Println(err.Error())

    return events.APIGatewayProxyResponse{
        StatusCode: http.StatusInternalServerError,
        Body:       http.StatusText(http.StatusInternalServerError),
    }, nil
}

// Similarly add a helper for send responses relating to client errors.
func clientError(status int) (events.APIGatewayProxyResponse, error) {
    return events.APIGatewayProxyResponse{
        StatusCode: status,
        Body:       http.StatusText(status),
    }, nil
}

func putItem(job *job) error {

    // create an aws session
    sess := session.Must(session.NewSession(&aws.Config{
        Region:   aws.String("us-east-1"),
        Endpoint: aws.String("http://localhost:8000"),
    }))

    // create a dynamodb instance
    db := dynamodb.New(sess)

    // marshal the job struct into an aws attribute value object
    jobAVMap, err := dynamodbattribute.MarshalMap(job)
    if err != nil {
        return err
    }

    input := &dynamodb.PutItemInput{
        TableName: aws.String("TEST_TABLE"),
        Item:      jobAVMap,
    }

    _, err = db.PutItem(input)
    return err
}

func main() {
    lambda.Start(CreateJobCommand)
}

Problem

I want to write a set of unit tests to test this function. In my mind, the first thing I need to do is mock the API Gateway request and the DynamoDB table, but I've no idea how to do this.

Questions

  1. Is there a mocking framework I should be using?
  2. If anyone knows of any documentation that would help on this topic could you point it out please? (My Google skills haven't revealed any yet)

Thanks

6 Answers

The way I do it is to pass dependencies in pointer receiver (since the handler's signature is limited) and use interfaces. Each service has corresponding interface. For dynamodb - dynamodbiface . So in your case in lambda itself you need to define a receiver:

type myReceiver struct {
    dynI dynamodbiface.DynamoDBAPI
}

change main to:

func main() {

    sess := session.Must(session.NewSession(&aws.Config{
        Region: aws.String("your region")},
    ))

    inj := myReceiver{
        dyn: dynamodb.New(sess),
    }
    lambda.Start(inj.CreateJobCommand)

change handler to

func (inj *myReceiver) CreateJobCommand(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)

and all subsequent calls to dynamodb APIs would need to be to through interface:

    _, err = inj.dynI.PutItem(input)

Then in your test function you need to mock the responses:


 type mockDynamo struct {
    dynI dynamodbiface.DynamoDBAPI
    dynResponse dynamodb.PutItemOutput

} 

func (mq mockDynamo) PutItem (in *dynamodb.PutItemInput) (*dynamodb.PutItemOutput , error) {
    return &dynamodv.dynResponse, nil
}


        m1: = mockDynamo {

            dynResponse : dynamodb.PutItemOutput{
            
            some mocked output
        } 


        inj := myReceiver{
            dyn: m1,         
        }

     inj.CreateJobCommand(some mocked data  for APIGateway request)

While mocking is a viable option, you may also consider e2e testing with dedicated aws account, find some examples including dynamodb, and api gateway

lambda_e2e

In addition to Adrian's answer:

Take a look at LocalStack. It provides an easy-to-use test/mocking framework for developing AWS-related applications by spinning up the AWS-compatible APIs on your local machine or in Docker. It supports two dozen of AWS APIs and DynamoDB and Lambda are among them. It is really a great tool for functional testing without using a separate AWS environment for that.

The question is bit old now but you could run dynamodb locally and use this aws-lambda-go-test module which can run lambda locally and can be used to test the actual response from lambda

full disclosure I forked and upgraded this module

Related