When using AWS SDK for JavaScript service objects we are typically facing two contradictory recommendations.
In one hand, as recommended by AWS Lambda Developer Guide, we want to create service objects during initialization of the lambda, so they stay in memory and we can reuse them in every invocation, and we then avoid the associated and hidden overhead of creating new objects for every request/response. Quoting original text:
When the handler finishes processing the first event, the runtime sends it another. The function's class stays in memory, so clients and variables that are declared outside of the handler method in initialization code can be reused. To save processing time on subsequent events, create reusable resources like AWS SDK clients during initialization. Once initialized, each instance of your function can process thousands of requests.
On the other hand, if you are unit testing your services, and using aws-sdk-mock for doing so, we have probably noticed that you have to initialize the AWS service you are mocking inside the function being tested. Otherwise, that service is not mocked. Official documentation is very clear on that:
NB: The AWS Service needs to be initialized inside the function being tested in order for the SDK method to be mocked
e.g for an AWS Lambda function example 1 will cause an error region not defined in config whereas in example 2 the sdk will be successfully mocked.Example 1:
var AWS = require('aws-sdk'); var sns = AWS.SNS(); var dynamoDb = AWS.DynamoDB(); exports.handler = function(event, context) { // do something with the services e.g. sns.publish }
Example 2:
var AWS = require('aws-sdk'); exports.handler = function(event, context) { var sns = AWS.SNS(); var dynamoDb = AWS.DynamoDB(); // do something with the services e.g. sns.publish }
Any caveout?
In order to cope with this two somehow incompatible requirements I found a valid pattern that seems to accommodate both requirements: the creation of a Singleton object that conditionally creates new clients when you are (unit) testing your code and you need those clients to be created in every test, BUT creates and reuses the client in other cases, as AWS recommends for general use in Lambda functions.
const enableLoggingDebug = Boolean(process.env.EnableLoggingDebug == "true");
const enableAwsMocking = Boolean(process.env.EnableAwsMocking == "true");
const REGION = process.env.REGION;
const AWS = require("aws-sdk");
let lambdaClient;
let dynamoDBdocumentClient;
exports.SmartLambda = () =>
enableAwsMocking
? new AWS.Lambda()
: typeof lambdaClient == typeof undefined
? (lambdaClient = new AWS.Lambda())
: lambdaClient;
exports.SmartDocumentClient = () =>
enableAwsMocking
? new AWS.DynamoDB.DocumentClient({ region: REGION })
: typeof dynamoDBdocumentClient == typeof undefined
? (dynamoDBdocumentClient = new AWS.DynamoDB.DocumentClient({
region: REGION,
}))
: dynamoDBdocumentClient;
AWS.config.logger = enableLoggingDebug ? console : AWS.config.logger;
So, with this sample Singleton object, every time you have to invoke a Lambda function or get a document from DynamoDB you just need to use SmartLambda
or SmartDocumentClient
and do the expected function call.
As said, instead of creating new objects to ensure your unit tests work, as we can see in the next piece of code:
const response = await new AWS.Lambda().invoke(params).promise();
You can use this singleton, so your code will look clean, will be efficient and your Jest unit tests will perfectly work:
const { SmartLambda } = require("./../../../utils/aws-clients");
// ...
const response = await SmartLambda().invoke(params).promise();
One final take away
Did you know that you can enable logging on SDK and see API calls you make with the SDK for JavaScript? Our singleton object above is conditionally activating this logging functionality based on a environment variable and sending it to console:
AWS.config.logger = enableLoggingDebug ? console : AWS.config.logger;
So, for example when a Lambda function is invoked you can see response code, elapsed time and other information about the request and response.
[AWS lambda 200 3.352s 0 retries] invoke({
FunctionName: 'arn:aws:lambda:us-east-1:123456789012:function:test-API',
Payload: '***SensitiveInformation***',
InvocationType: 'RequestResponse'
})
You can see more information about this in the AWS SDK for JavaScript Developer Guide
Top comments (0)