First, create a new project and generate a new package.json
file for it, running the following commands:
mkdir testing
cd testing
npm init
Install dependencies needed for the project.
npm install -D serverless-offline serverless-mocha-plugin
Create a function that will multiply two numbers and include it in a file called logic.js
.
module.exports.multiplyNumbers = (x, y) => {
return x * y;
}
Create a handler function that will multiply two numbers and include it in a file called handler.js
.
const logic = require('./logic');
module.exports.multiplyNumbersForTest = (event, context, callback) => {
// event.body will be set in case of post request and event will be set in case of payload passed by other lambda
const vars = event.query || event;
const response = {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*', // Required for CORS support to work
},
body: JSON.stringify({
result: logic.multiplyNumbers(vars.x, vars.y) || 0,
input: event,
}),
};
callback(null, response);
};
We intentionally separated logic from the actual lambda handler as this allows you to create both unit tests for business logic and integration tests on actual lambda functions.
In this tutorial, we are gonna use serverless-mocha-plugin
to create and run serverless tests.
Let's create a serverless.yml
file.
service: testing
provider:
name: aws
runtime: nodejs8.10
region: eu-west-1
profile: default
memorySize: 256 # optional, in MB, default is 1024
stage: dev
plugins:
- serverless-offline
- serverless-mocha-plugin
functions:
multiplyNumbersForTest:
handler: handler.multiplyNumbersForTest
events:
- http:
integration: lambda
path: multiply-number-for-test
method: get
cors: true
Now, if you run serverless
you should see three additional commands available from the serverless-mocha-plugin - create test
, create function
and invoke test
.
Let's create a test for our multiplyNumbersForTest
function. Simply run:
sls create test --function multiplyNumbersForTest
This will create multiplyNumbersForTest.js
file in test
folder.
Now, you can try to run an empty with with sls invoke test
command. Tests should pass.
Now, we need to write the actual test. Add this test instead of generated test:
it('should fail if the result is wrong', () => {
const x = 4234;
const y = 4334;
const result = 18350156;
return wrapped.run({
query: { x, y }
}).then((response) => {
expect(response).to.not.be.empty;
expect(response.body).to.not.be.empty;
expect(JSON.parse(response.body).result).to.be.eql(result);
});
});
Note that we use pre-calculated values to assert if our lambda calculates it correctly. The test we just wrote is an e2e test that tests the "contract" between the front-end and our lambda. The object that is passed in wrapper.run
is essentially our event
that we access in lambda. The approach that is taken by serverless-mocha-plugin
is to invoke the exported lambda directly in the code. It doesn't do the actual HTTP request to our lambda. It is reasonable approach as not all lambdas listen to HTTP events. There are many other events the lambda can listen to.
Note the mocha before
in the test file. You can connect to database, Redis or any other private datasource there. In case you would like to share the connection between different files, you can use beforeAll
handler to open the connection and afterAll
to close the connection.
Above you could see a simple example on how to create the simplest test that will make sure our lambdas work as expected. In real projects, unless you are the God-mode programmer, you have to write hundreds of tests and follow certain best practices for your code to be reliable and maintainable. In Reason, we try to follow these principles:
Write integration end-to-end tests to test the "contracts" between front-end and backend. One could argue that integration tests by itself cannot be reliable indicator that the code wouldn't break. We agree with that completely. However, it allows us to be confident that the front-end that relies on lambda APIs or any other components of the cloud ecosystem that use lambdas will not break after new updates to the business logic inside any of the lambdas. The main problems with integration tests:
Therefore, the next principle:
Write unit tests for whatever cannot be covered with integration tests. You should break your logic into smaller pieces that can be easily tested separately.
In the ideal scenario of unlimited resources one would write both integration and unit test with 100% coverage. For real-world projects under heavy time constraints it might be reasonable to balance between integration and unit tests.
What makes AWS Lambda special compared to traditional node.js development is that you can't simulate the exact AWS Lambda environment on your local machine. Good thing about Serverless framework is that in most cases you can deploy the copy of your Lambda stack to another account and just launch tests in the cloud.
For some of the projects where most lambdas are HTTP based, instead of using serverless-mocha-plugin
, you can just write tests that invoke lambdas using HTTP protocol (for example with request
library). In that case, you can be sure that if you ever decide to migrate logic in your lambdas to an express server on EC2 or to a private to VPS due-to cost reasons or regulators requirements, you can just change the configuration of the endpoint in tests and it will just work. With module.exports invocation testing style like in the example above, you are bound to the lambda stack and will have to rewrite code in case you migrate.
First of all, it is easier to unit test separate parts of the logic in that way. Secondly, the logic can be re-used in other services. Third, the service endpoints can be changed at any time using the same logic. Imagine, in version 1.0 of API you had all the endpoint /orderAdd
, orderEdit
and orderComplete
. In case you separate the logic, you can easily migrate to API version 2.0 with order/add
, order/edit
and order/complete
endpoints.
Further reading: