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
:
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:
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.
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
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-app
Sample SAM Template for sam-app
Globals:
Function:
Timeout: 5
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello-world/
Handler: hello-world
Runtime: go1.x
Tracing: Active
Events:
CatchAll:
Type: Api
Properties:
Path: /hello
Method: POST
Environment:
Variables:
DYNAMODB_TABLE: !Ref VoteTable
VoteTable:
Type: AWS::Serverless::SimpleTable
Properties:
TableName: votes
PrimaryKey:
Name: ID
Type: String
Outputs:
HelloWorldAPI:
Description: "API Gateway endpoint URL for Prod environment for First Function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
main.go
Click to expand
package main
import (
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
)
type dependency struct {
ddb dynamodbiface.DynamoDBAPI
table string
}
// Record represents one record in the DynamoDB table
type Record struct {
ID string
Body 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"),
}
}
// Create a new record from the request
r := Record{
ID: request.RequestContext.RequestID,
Body: request.Body,
}
// Marshal that record into a DynamoDB AttributeMap
av, err := dynamodbattribute.MarshalMap(r)
if err != nil {
return events.APIGatewayProxyResponse{}, err
}
// Save the AttributeMap in the given table
_, err = d.ddb.PutItem(&dynamodb.PutItemInput{
TableName: aws.String(d.table),
Item: av,
})
if err != nil {
return events.APIGatewayProxyResponse{}, err
}
return events.APIGatewayProxyResponse{
StatusCode: 200,
}, nil
}
func main() {
d := dependency{}
lambda.Start(d.LambdaHandler)
}
main_test.go
Click to expand
package main
import (
"testing"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
)
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")
}
})
}
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.