Unit & Integration Testing in Nodejs for AWS SAM(Serverless Application Model) using Jest

Aug 02, 2021

Have you ever found yourself stuck while writing unit & integration test cases for your serverless projects? If yes, then I have got your back. Automated testing is the key to creating clean abstractions and fast feedback. It helps when the size and complexity of your serverless project grow. In this article, I have explained how to write Unit & Integration test cases in Nodejs, and AWS SAM simply using Jest.

CodeBase Testing

Code-based testing corresponds to the testing carried out on code development, code inspection, and unit testing in the software development process.

  1. Unit Testing: In a summarized way, this testing tests a single fully isolated unit of the application.
  2. Integration Testing: This testing tests the interaction of a unit along with its dependencies. e.g., a function that calls another function, which means, the test results also depend on the function being called within the parent function.

Jest- An Open Source Framework

Jest is an open-source framework built for testing JavaScript. It is created and maintained by Facebook. Jest was built with multiple layers on top of Jasmine by keeping some of the good parts from Jasmine. The best part that I love about Jest is the human-friendly framework & lightweight. It has gained attention because of its well-supported and fast testing behaviour.

Benefits of Jest

There are lots of testing frameworks available like Mocha, Chai, Enzyme, AVA, Jasmine, etc. But, Jest is more popular because of

  1. Fast framework
  2. Simple configuration
  3. Easy to use
  4. In-built Mocks and spies
  5. In-built Coverage reports
  6. Runs tests in parallel processes
  7. Open-source
  8. Comes with wide APIs
One of Jest’s philosophies is to provide an integrated “zero-configuration” experience.

Project Setup

First, we need to create a project by using the "sam init" command

PS C:\Jest> sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location 
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1


Which runtime would you like to use?
        1 - nodejs12.x
        2 - python3.8
        3 - ruby2.7
        4 - go1.x
        5 - java11
        6 - dotnetcore3.1
        7 - nodejs10.x
        8 - python3.7
        9 - python3.6
        10 - python2.7
        11 - ruby2.5
        12 - java8.al2
        13 - java8
        14 - dotnetcore2.1
Runtime: 1


Project name [sam-app]: jest-testing


Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates


    -----------------------
    Generating application:
    -----------------------
    Name: jest-testing
    Runtime: nodejs12.x
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .


    Next steps can be found in the README file at ./jest-testing/README.md

After creating the project setup and adding some required files & folders, our project structure will look like this.

The project setup is ready. Now, let's go for testing...

Unit Test Cases

Before writing test cases we need to install jest in our project as a devDependencies.

PS C:\Jest\jest-testing> npm install jest --save-dev 


added 327 packages, and audited 328 packages in 30s


24 packages are looking for funding
  run `npm fund` for details


found 0 vulnerabilities

Now, we will write unit test cases for the randomNumeber.js file

We will create a file inside the test/unit folder with the same name ending with .test.js and that will be randomNumeber.test.js. Because jest only identifies .test extension files.

Writing the test cases also depends on what functionality we want to test. We will be writing test cases for the above random number function.

We can see there are 3 test cases written

  1. The first test case would be testing all possible exceptions like output should not be a string, null, undefined & {}.
  2. In the second test case, we are testing the type of output it should be a number, we are using toBe('number') for exact matching.
  3. In the third test case, we test the length of the number using the toHaveLenth() jest function.

Because we are using Babeljs in this project, therefore we need to install some Babel dev dependencies. After that, we can run the npm test ( need to configure in the package.json file).

We can observe that test suites mean the total number of files, Tests mean the total number of test cases across the project & we can also see the total, estimated time.

Integration Test Cases

For integration testing, we need to add some more files to our project, so now the project structure will look like this.

We can see new files such as userService.js, userService.test.js & eventGenerater.js. and, other than these we also added some dependencies.

In this project, we are using webpack for creating a build, MySQL database, for authentication using JWT.

now we will write test cases for the userService.js file.

import promisePool from '../utils/dbConnection'
import { jwtDecoder } from '../utils/jwtDecoder'
import { formatResponse } from '../utils/responseFormatter'



const addUser = async (event) => {
  const request = JSON.parse(event.body)
  const insertUser = 'INSERT INTO user (name, phone, email) VALUES(?, ?, ?)'
  const dataFromDecoder = jwtDecoder(event)
  if (typeof dataFromDecoder === 'string')  return formatResponse(null, { code: 400, message: dataFromDecoder })
  if(!request.email) return formatResponse(null, { code: 400, message: 'email is required' })
  try {
      await promisePool.query(insertUser, [request.name, request.phone, request.email])
      return formatResponse({ status: 200, message: 'user created successfully' }, null)
    
  } catch (error) {
    return formatResponse(null, { message: error.message, code: 400 })
  }
}


export default {
  addUser
}

Now, we will create a file inside the test folder with userService.test.js & one more file eventGenerator.js for formatting the APIGatewayRequest.

Test cases of this addUser Lambda function look like

import userService from '../../../src/services/userService'
const eventGenerator = require('../testUtils/eventGenerator')
var token = 'jwtToken'
describe('userService  integration tests', () => {
  test('it shoudl take a body and return an API Gateway response', async () => {
    const event = eventGenerator({
      body: {
        name: 'neetesh',
        phone: '8465656111',
        email: 'neetesh@gmail.com'
      },
      headers: {
        Authorization: token
      }
    })
    const res = await  userService.addUser(event)
    expect(typeof res).toBe('object')
    expect(res).toBeDefined()
    const response = JSON.parse(res.body)
    expect(typeof response).toBe('object')
    expect(res.statusCode).toBe(200)
  })
  test('API Gateway response email is not provided', async () => {
    const event = eventGenerator({
      body: {
        name: 'neetesh',
        phone: '8465656111',
        // email: 'neetesh@gmail.com'
      },
      headers: {
        Authorization: token
      }
    })
    const res = await  userService.addUser(event)
    expect(typeof res).toBe('object')
    expect(res).toBeDefined()
    const response = JSON.parse(res.body)
    expect(typeof response).toBe('object')
    expect(res.statusCode).toBe(400)
  })
  test('API Gateway response when provide wrong inputs', async () => {
    const event = eventGenerator({
      body: {},
      headers: {
        Authorization: token
      }
    })
    const res = await  userService.addUser(event)
    expect(typeof res).toBe('object')
    expect(res).toBeDefined()
    expect(res.statusCode).toBe(400)
  })
  test('aad user check if token is not provided ', async () => {
    const event = eventGenerator({
      body: {
        name: 'neetesh',
        phone: '8465656111',
        email: 'neetesh@gmail.com'
      },
      headers: {
        Authorization: null
      }
    })
    const res = await  userService.addUser(event)
    expect(typeof res).toBe('object')
    expect(res).toBeDefined()
    expect(res.statusCode).toBe(400)
  })
})

You can see that we have written described function inside it. We are testing four test cases for different situations.

  1. In the first test case, we are adding user data to the database, we used an event generator for formatting requests. We are testing the type of res object, defined & statusCode should be 200.
  2. In the second test case, we are testing mandatory field email, if the email is not provided then it should throw an error with status code 400.
  3. In the third test case, we are testing inputs, whether provided inputs properly or not.
  4. In the fourth test case, we are testing authentication through tokens.

After running these test cases the output looks like this.

There are now a total of two test suites, 7 test cases for both units & an integration test.

Now, we will test some more use cases, the ones we added inside the package.json file.

We will see the test results are more readable & understandable.

Code Coverage

Code coverage is a very important part of the testing because with the help of that report we can identify how much our code is covered by test cases.

To implement code coverage, we will just add one more --coverage keyword in the package.json file just after verbose.

Now the test case result will look more descriptive also.

You can see the whole report with all descriptions like percentage of statements, branch, functions, lines, and uncovered lines.

We can see a web page report of these results, when we run the coverage script it will create a folder inside the root directory with the name coverage.

Inside coverage, one index.html. We can open the index.html file in the browser and see the result.

We achieved code coverage 96.88% Statements 31/32, 86.36% Branches 19/22, 100% Functions 5/5,96.67% Lines 29/30

We will see one more thing if we want to check code coverage inside any particular file.


Here, we can see the red coloured line, this line is not covered by our test cases. But don't worry our code will be accepted by the project manager, the reason being, our code coverage is more than 80%.

Conclusion

Complex modern systems quickly become unmanageable and untestable manually. So, it is crucial to have a fast and easy-to-use framework for unit and integration testing. Obviously, writing testable code requires some discipline, concentration, and extra effort. I played around with testing lately and in this blog, I tried to test the functions of my SAM application, using Jest, an open-source fast framework.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.