4

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

madhead
  • 31,729
  • 16
  • 153
  • 201
GreenyMcDuff
  • 3,292
  • 7
  • 34
  • 66

6 Answers6

7

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)
0

Please, check whether running dynamo-db in a docker will not help you to implement your test.

Check: connecting AWS SAM Local with dynamodb in docker

You can also, quite easily, pass the event to handler in your test.

Skarab
  • 6,981
  • 13
  • 48
  • 86
0

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

Adrian
  • 1,973
  • 1
  • 15
  • 28
0

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.

madhead
  • 31,729
  • 16
  • 153
  • 201
0

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

Yogesh
  • 4,546
  • 2
  • 32
  • 41