MERN Stack: test Node.js and Express with Jest

JavaScript backend testing

·

8 min read

MERN Stack: test Node.js and Express with Jest

Introduction

We will be using the code from the 2nd part of this series.

You can find the code in this Github repository, note that you are on the testing branch.

hmu @tekbog if you find any bugs or want to annoy me.

About testing and QA

image.png

People still fight over definition and how it should be done, however this article from IBM might give you some insight if you are unfamiliar with software testing.

To largely simplify testing we can define some concepts:

  • Unit tests: as the definition implies we will test simple units of code, one function at a time.
  • Integration tests or I&T: is when you test several functions or different pieces of code that work together
  • End to End testing: as the name implies you fully test your whole application, often as an emulated user
  • Test Driven Development: write the test for the function you want, then write the function

A lot of different tools exist for almost any language, in JavaScript the most famous ones are Jest, Mocha and Cypress for E2E. However the overall most famous E2E tool might be Selenium.

Git

I'm not going to go into detail here, if you unfamiliar with Git and want to learn more about check out this tutorial from Atlassian. Start at the beginning if you are confused. A few key points for newbies:

  • Git is a version control tool
  • Github is the hub that hosts Git projects, it's owned by others and there are other companies such as Bitbucket and Gitlab that do the same.

I'm using my old project so I'm going to make a new branch with

git checkout -b testing main

and stay here as this will be the branch for this article.

Jest (or Mocha)

We will be using Jest since we want to focus on React, however if you are working with Node.js and Mongoose it's discouraged even in the official Mongoose docs so feel free to use an alternative like Mocha. We will use Jest as our application isn't complex enough to run into problems and we will focus on React in the future.

Let's install Jest

npm install --save-dev jest

Afterwards inside package.json modify the test script

"test": "jest --verbose"

You should have this:

image.png

Start testing

Now that we have Jest installed let's make some dummy tests to see how it works.

NB: I will be using normal import/export as ES6 module exports might give you some issues.

Let's make a helper file inside utils folder that will contain our function we want to test test_helper.js:

//return the sum of two numbers
const sumFunction = (a,b) => {
    return a+b
}

module.exports = {
    sumFunction
}

Next we will make a folder called tests and inside a file called dummy.test.js where we will test the function above:

const { sumFunction } = require("../utils/test_helper")
//tests take a description and a function
test('test sumFunction, expected to pass', () => {
    const a = 5
    const b = 6
    const result = sumFunction(a, b)
    //through expect we indicate the expected result of the test
    //the .toBe method expects a specific value you pass
    //https://jestjs.io/docs/expect#tobevalue
    expect(result).toBe(11)
})
test('test sumFunction, expected to fail', () => {
    const a = 5
    const b = 6
    const result = sumFunction(a, b)

    expect(result).toBe(12)
})

Note that .test is needed so Jest knows to read your tests from this file. Now let's do

npm run test

This should be the result:

image.png

Check out many of the Expect methods Jest has here

Testing a RESTful API

We will be using ES6 modules so we need to make some changes in our package.json as follows:

"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --verbose --runInBand --forceExit"

Jest gives you more information here.

We will be using Supertest, so let's install it

npm install supertest

Now let's make a file inside tests called api.test.js where we will be importing everything we need to make our tests:

import supertest from 'supertest'
import { app } from '../app.js'
//use the supertest object as our API
const api = supertest(app)

import { Item } from '../models/item'

Then remember to run our server with npm start dev so it restarts every time we make changes and open a new window terminal so we can type our Jest commands.

GET test

Since we are getting our data from a server we will need to use async functions, keep that in mind.

This is a test for our GET call expecting a 200 status:

test('GET call', async () => {
    await api
        .get('/api/items')
        .expect(200)
})

You can run this test through this command

run npm test -- -t "GET call"

That's how you run individual tests.

In order to make the test a bit more complete we will also add a method to expect JSON data:

test('GET call', async () => {
    await api
        .get('/api/items')
        .expect(200)
        .expect('Content-Type', /application\/json/)
})

Your api.test.js file should look like this by now

import supertest from 'supertest'
import { app } from '../app.js'
//use the supertest object as our API
const api = supertest(app)

//run npm test -- -t "GET call"
//test GET or READ call on localhost:3001/api/items endpoint
test('GET call', async () => {
    await api
        .get('/api/items')
        .expect(200)
        .expect('Content-Type', /application\/json/)
})

POST test

In order to interact with the database properly we will use our Mongoose model

import { Item } from '../models/item'

Afterwards we will proceed to make a POST call

test('POST call', async () => {
    //build a new item
    const newItem = {
        description:"sent from Jest!",
        likes: 10
    }
    //we send the item object to the DB through the API
    //we expect a successful result
    await api
        .post('/api/items')
        .send(newItem)
        .expect(201)
    //get all the items in our DB
    const items = await Item.find({})
    //let's check that the last item added was indeed newItem object
    //it should contain the description "sent from Jest!"
    expect(items[items.length-1].description).toBe("sent from Jest!")
})

GET item by id

In this test we are going to check if the GET route by specific id works as expected.

test('GET one', async () => {
    //get all the items
    const items = await Item.find({})
    //get the the first item parsed to JSON
    const firstItem = items[0].toJSON()
    //get the result expecting success and JSON data
    const resItem = await
        api.get(`/api/items/${firstItem.id}`)
        .expect(200)
        .expect('Content-Type', /application\/json/)
    //check if the item has the same id and the route works as expected
    expect(resItem.body.id).toEqual(firstItem.id)
})

Check all items have an id

Let's add one more simple test

    test('check all items have ids', async () => {
        //get all the items
        const items = await api.get('/api/items')
        //check that every item in our DB has the id property
        for(const item of items.body){
            expect(item.id).toBeDefined()
        }
    })

Refactoring GET calls

Let's use describe to put our three GET calls together as follows:

describe('GET calls', () => {
    //run npm test -- -t "GET call"
    //test GET or READ call on localhost:3001/api/items endpoint
    test('GET call', async () => {
        await api
            .get('/api/items')
            .expect(200)
            .expect('Content-Type', /application\/json/)
    })
    //npm test -- -t "GET one"
    //GET item by id
    test('GET one', async () => {
        //get all the items
        const items = await Item.find({})
        //get the the first item parsed to JSON
        const firstItem = items[0].toJSON()
        //get the result expecting success and JSON data
        const resItem = await
            api.get(`/api/items/${firstItem.id}`)
            .expect(200)
            .expect('Content-Type', /application\/json/)
        //check if the item has the same id and the route works as expected
        expect(resItem.body.id).toEqual(firstItem.id)
    })
    test('check all items have ids', async () => {
        //get all the items
        const items = await api.get('/api/items')
        //check that every item in our DB has the id property
        for(const item of items.body){
            expect(item.id).toBeDefined()
        }
    })
})

Afterwards we can use this cmd to call this block of tests

npm test -- -t "GET calls"

If everything is successful we will get this in the console:

image.png

DELETE

Let's check that we can delete an item and all its data is gone

test('DELETE item', async () => {
    //get items and parse the one you want to delete to JSON
    const itemsAtStart = await Item.find({})
    const itemToDelete = itemsAtStart[0].toJSON()
    //delete the item by id
    await api
        .delete(`/api/items/${itemToDelete.id}`)
        .expect(204)
    //get all items from the database again
    const itemsNow = await Item.find({})
    //check if the number of current items is one less than before
    expect(itemsNow).toHaveLength(itemsAtStart.length-1)
    //get an array of all the descriptions inside the DB
    //could get any other info like the id
    const itemsDescriptions = itemsNow.map(i => i.toJSON().description)
    //expect the description from the deleted item to not be there
    expect(itemsDescriptions).not.toContain(itemToDelete.description)
})

beforeEach

If you want to run any code before tests start you need to use beforEach. In our case we want our database to reset every time we start testing so let's make some data to insert inside a file called test_helper.js

const initialItems = [
    {
        description: "first item",
        likes: 5
    },
    {
        description: "second item",
        likes: 7
    },
    {
        description: "third item",
        likes: 10
    }
]

export {
    initialItems
}

then import that data and use it to reset our database every time we start testing. We delete everything and then insert our initial data again:

beforeEach(async () => {
    await Item.deleteMany({})
    await Item.insertMany(initialItems)
})

afterAll

If you want to run something after all our tests are done then use afterAll

afterAll(() => mongoose.connection.close())

In our case we just close the connection to the database.