Testing

One of the most common usecases for Node.js is writing test for Node.js and Frontend apps. Because Node.js can run outside the browser, it's perfect for CI environments and testing automations.

Basic unit tests

Unit test will test little chunks of your code in isolation to ensure they behave has intended. Node.js ships with the assert module. This module gives us so many utilities that allow us to create expectations of on our code. When those expectations aren't met, assert will throw an error telling us why. This is perfect for testing!.

First let's create some code to test:

// myLib.mjs
export const add = (num1, num2) => num1 * num2

Next let's write some assetions on this code

// test.mjs
import assert from 'assert'
import { add } from './myLib.mjs'

try {
  console.log('add() should add two numbers ')
  assert.strictEqual(add(2, 5), 7)
  console.log('  ✅ passed')
} catch (e) {
  console.log('  🚫 fail')
  console.error(e)
}

Now run the test file with:

node test.mjs

You should get an output that describes how this assertion fails. Because our add function actully multiplies two numbers and instead of adding them. So either our tests is off or the code is wrong. Let's say the code is wrong, so lets fix that and run the test again and we should see that it passes now.

Assert is great but there are some amazing tools and libs built around it that make writing and reading tests satisfying. One of those tools that the comminity has adopted is called jest.

Using Jest

Jest is a testing lib created by Facebook. Its a wonderful tesitng lib for any situation. Let's give it a try! Create a new package with npm and install jest

npm install jest --save-dev

Notice we used the --save-dev flag this time. We want to save jest in our package.json but as a dev dependency. Because our code does not depend on jest at runtime to execute. When your app gets deployed or installed by another dev, those machines will only NEED to install dependencies and not dev dependency. Saving space and time.

Next add a test script in the package.json:

"scripts": {
  "test": "jest"
}

Now we need some code to test.

// myLib.mjs

const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const users = [{ email: 'goku@test.com', id: 1, name: 'Goku', verified: false }]

const getNewUser = async id => {
  await delay(100)
  const user = users.find(u => u.id === id)

  if (!user) throw new Error('User does not exist')
  return user
}

const mapObjectToArray = (o, cb) => {
  const results = []

  for (const [k, v] of Object.entries(o)) {
    results.push(cb(k, v, o))
  }

  return results
}

module.exports = { getNewUser, mapObjectToArray }

At this time, the latest version of jest lacks support for ES modules without some setup, we're using commonjs modules. The next version of Jest will support these though. We have some async code and a function with a callback. These would've been tedious to test with just assert alone. Jest makes this easy.

// lib.spec.js
const { mapObjectToArray, getNewUser } = require('./myLib')

describe('getNewUser', () => {
  test('user does exist', async () => {
    const user = await getNewUser(1)

    expect(user).toBeTruthy()
    expect(user.verified).toBe(false)
  })

  test('user does not exist', async () => {
    expect.assertions(1)

    try {
      await getNewUser(3)
    } catch (e) {
      expect(e.message).toBe('User does not exist')
    }
  })
})

describe('mapObjectToArray', () => {
  test('callback gets called for each value', () => {
    const mock = jest.fn()

    mapObjectToArray({ a: 1, b: 1, c: 1 }, mock)
    expect(mock.mock.calls.length).toBe(3)
  })

  test('callback gets the right args', () => {
    const mockCb = jest.fn()
    const o = { a: 1, b: 1, c: 1 }

    mapObjectToArray(o, mockCb)
    const firstCall = mockCb.mock.calls[0]

    expect(firstCall[0]).toBe('a')
    expect(firstCall[1]).toBe(1)
    expect(firstCall[2]).toBe(o)
  })
})

Notice how we didn't have to require or import a jest module but yet we have access to all these new globals like describe and test. That's because this test file will be executed with the jest cli which will supply these values during runtime. If you executed this file with the node runtime, you'll get errors.

Most Node.js testing frameworks follow this format of describing a tes suite, usually a function. From there, we make different calls to test for each assertion we want to claim on that code. Sometimes, you might make many assertions in one test call. There is no one right way.

Now, lets run the test. Jest will look for any file with the .(spec|test).js extension and run it. You can change that if you'd like of course.

npm test

What we get back here is much more pleasant compared to our plain test we made with assert. This is the default reporter, but you can add more reporters like html or json. Or make your own.