When writing tests for a service you’ll often find that there are other dependent services that may need to be called in some way or another. You won’t want to actually invoke the real service during test, but rather ‘stub’ out the dependent service call function. Sinon stub provides an easy and customisable way to replace these external service calls for your tests.
Practical Example: AWS SQS sendMessage Sinon Stub
Let’s say you have an AWS Lambda function that drops a message onto an SQS queue. To test this function handler, your test should invoke the handler and verify that the message was sent.
This simple case already involves an external service call – the SQS sendMessage
action that will drop the message onto the queue.
Here is a simple NodeJS module that wraps the SQS sendMessage
call.
// sqs.ts import AWS = require("aws-sdk"); import { AWSError } from "aws-sdk"; import { SendMessageRequest, SendMessageResult } from "aws-sdk/clients/sqs"; import { PromiseResult } from "aws-sdk/lib/request"; const sqs = new AWS.SQS({apiVersion: '2012-11-05'}); export function sendMessage(messageBody: string, queueUrl: string) : Promise<PromiseResult<SendMessageResult, AWSError>> { var params = { QueueUrl: queueUrl, MessageBody: messageBody, } as SendMessageRequest; return sqs .sendMessage(params) .promise() .then(res => res) .catch((err) => { throw err; }); }
The actual Lambda Handler code that uses the sqs.ts module above looks like this:
// index.ts import { sendMessage } from './sqs'; import { Context } from 'aws-lambda'; export const handler = async (event: any, context?: Context) => { try { const queueUrl = process.env.SQS_QUEUE_URL || "https://sqs.eu-west-2.amazonaws.com/0123456789012/test-stub-example"; const sendMessageResult = await sendMessage(JSON.stringify({foo: "bar"}), queueUrl); return `Sent message with ID: ${sendMessageResult.MessageId}`; } catch (err) { console.log("Error", err); throw err; } }
Next you’ll create a Sinon stub to ‘stub out’ the sendMessage
function of this module (the actual code that the real AWS Lambda function would call).
Setup an empty test case that calls the Lambda handler function to test the result.
// handler.spec.ts import * as chai from 'chai'; import * as sinon from "sinon"; import { assert } from "sinon"; import * as sqs from '../src/sqs'; import { handler } from '../src/index'; import sinonChai from "sinon-chai"; import { PromiseResult } from 'aws-sdk/lib/request'; import { SendMessageResult } from 'aws-sdk/clients/SQS'; import { Response } from 'aws-sdk'; import { AWSError } from 'aws-sdk'; const expect = chai.expect; chai.use(sinonChai); const event = { test: "test" }; describe("lambda-example-sqs-handler", () => { describe("handler", () => { it("should send an sqs message and return the message ID", async () => { // WHEN process.env.SQS_QUEUE_URL = "https://sqs.eu-west-1.amazonaws.com/123456789012/test-queue"; const result = await handler(event); // THEN expect(result).to.exist; expect(result).to.eql(`Sent message with ID: 123`); }); }); });
Right now running this test will fail due to the test code trying to call the sqs.ts module’s code that in turn calls the real SQS service’s sendMessage
.
Here is where Sinon stub will come in handy. You can replace this specific call that sqs.ts makes with a test stub.
In the describe handler section, add the following just before the ‘it‘ section.
const sendMessageStub = sinon.stub(sqs, "sendMessage"); let stubResponse : PromiseResult<SendMessageResult, AWSError> = { $response: new Response<SendMessageResult, AWSError>(), MD5OfMessageBody: '828bcef8763c1bc616e25a06be4b90ff', MessageId: '123', }; sendMessageStub.resolves(stubResponse);
The code above calls sinon.stub()
and passes in the sqs module object, as well as a string (“sendMessage” in this case) identifying the specific method in the module that should be stubbed.
An optional promise result can be passed in to resolves()
to get the stub to return data for the test. In this case, we’re having it return an object that matches the real SQS sendMessage
return result. Among other things, this contains a message ID which the Lambda function includes in it’s response.
Add a test to verify that the stub method call.
assert.calledOnce(sendMessageStub);
If you run the test again it should now pass. The stub replaces the real service call. Nice!
Conclusion
Replacing dependent service function calls with stubs can be helpful in many ways. For example:
- Preventing wasteful, real service calls, which could result in unwanted test data, logs, costs, etc…
- Faster test runs that don’t rely on network calls.
- Exercising only the relevant code you’re interested in testing.
Sinon provides a testing framework agnostic set of tools such as test spies, stubs and mocks for JavaScript. In this case, you’ve seen how it can be leveraged to make testing interconnected AWS services a breeze with a Lambda function that calls SQS sendMessage
to drop a message onto a queue.
Feel free to Download the source code for this post’s example.