Introduction

Jest makes unit testing painless and easier compared to using other frameworks such as using Mocha, chai, and Sinon.

Let’s dive right in!

What jest offers

Jest acts both as a test runner(runs your tests) and acts as a test suite(providing assertions using matchers). With Jest, everything becomes easy to set up as it provides an all-in-one solution for carrying out unit testing.

All code for all examples in this article can be found in this unit testing with jest and is all open for any use.

1. Setting up jest

  1. Create a new project
mkdir jest-unit-testing
cd jest-unit-testing
npm init -y
  1. Add jest as a devDependency to the project
npm install --save-dev jest
  1. Add jest globally to be able to use later to generate jest.config.js files

  2. Generate a jest.config.js file for controlling options for running your tests using jest such as collectCoverage among other powerful options to choose from

npm install -g jest
# inside the project root
jest --init 

2. Our first test case

To confirm that jest has been set up correctly and working let’s write a dummy test case, but first..

Globals

Jest offers globals- describe() and expect so one does not have to explicitly import these inside their *.test.js files. It’s possible but not necessary.

It’s also a convention by jest to name files with a .test.js or .test.ts if using TypeScript to separate source files from test files, jest runs any file with this naming convention but can also be overridden in the config file.

  1. create a src/ folder inside the project, create an index.js and add the following lines:
const sum = (a, b) => a + b
module.exports = sum
  1. Create index.test.js and add the following lines:
describe('addition', () => {
  test('adds 2 numbers and returns their sum', () => {
    expect(sum(2,2)).toBe(4)
  })
})

Explanation

describe wraps all our testcases (test) under one descriptive name addition which is called a test suite. expect(expression) is the assertion portion and .toBe(expected) is the matcher. Jest offers a lot of matchers to match different assertions.

Here are a few common matchers that jest offers:

const square = (a) => a * a
test('square of 5 is 25', () => {
  expect(square(5)).toBe(25)
})

describe('Test Matchers', () => {
  test('null', () => {
    const n = null
    expect(n).toBeNull()
    expect(n).toBeDefined()
    expect(n).not.toBeUndefined()
    expect(n).not.toBeTruthy()
    expect(n).toBeFalsy()

  })

  test('zero', () => {
    const z = 0
    expect(z).not.toBeNull()
    expect(z).not.toBeUndefined()
    expect(z).not.toBeTruthy()
    expect(z).toBeFalsy()
  })

  test('adding floating point numbers', () => {
    const value = 0.1 + 0.2
    console.log(value)
    //expect(value).toBe(0.3);           This won't work because of rounding error
    expect(value).toBeCloseTo(0.3) // This works.
  })
  
  test('should test arrays', () => {
    const shoppingList = [
      'diapers',
      'kleenex',
      'trash bags',
      'paper towels',
      'milk'
    ]
    expect(shoppingList).toContain('milk')
  })

  test('should test strings', () => {
    expect('Naftali').toMatch(/Naf/)
  })

  test('should test exceptions', () => {
    const compileAndroidCode = () => {
      throw new Error('JDK not found in path')
    }

    expect(() => compileAndroidCode()).toThrow(Error)
  })
})

Each matcher should narrow down and be close to the expected behavior as much as possible.

Testing async behavior

Testing promises and async behavior is quite straightforward. Here are a few gotchas that will save you a lot of head-scratching:

  • Always wrap your asynchronous calls with try...catch if using async/await to avoid unhandled promise rejection.
  • Ensure that promises resolve and be sure to always append the .catch(err=>{}) when consuming a Promise using the .then syntax.

Doing the above solves one hidden problem that I noticed: In jest if asynchronous code doe not resolves or runs into the common unhandled promise rejection error, the tests timeout. The error looks like this: Timeout of 5000ms exceeded causing the test to fail. This has been asked a lot on Stackoverflow.

Test async behavior

Assuming we are fetching data from a web API over HTTP. We’d like to test that whatever the API returns are exactly what we expected(We won’t use mocking because, if the API changes the tests would still work).

  1. To test asynchronous behavior we prepend async keyword to the callback passed to test(). The test suite looks more like:
const fetchTodos = async () => {
  try {
    const res = await fetch('httpss://jsonplaceholder.typicode.com/todos/1')
    const todos = await res.json() // parse response back to plain Js objects
    return todos
  } catch(e) {
    console.error(`fetching failed with`, e)
  }
}

The tests case:

import fetchTodos from './fetchTodos'

describe('test async', () => {
  test('returns API response todos', async() => {
    const todos = await fetchTodos()
    expect(todos).toBeArray()
  })
})

Testing async behavior becomes as trivial as above. One thing, go back to fetchTodos.js and remove the try...catch block, run the test and see what happens to the test results.

Extending the matchers with community provided matches

Jest offers an option to extend the matchers using custom matchers. However, there are custom matchers provided by the lovely community packaged as jest-extended npm module.

  1. To extend jest matchers, add jest-extended as a devDependency,
npm install --save-dev jest-extended
  1. Update jest.config.js to enable extending in all/single testfiles.

jest.config.js

const config = {
  testEnvironment: 'node',
  verbose: true,
  automock: false,
  clearMocks: true,
  collectCoverage: true,
  // https://github.com/jest-community/jest-extended: Extended matchers
  setupFilesAfterEnv: ['jest-extended/all'],
}

exports = config
  1. Extend matchers inside your testfiles:
const matchers = require('jest-extended')
expect.extend(matchers)

jest-extended provides a lot of matchers that are useful especially if default matchers do not cover the behavior to be asserted. Read more about these matchers here jest-extended-matchers

Summary

Jest makes testing a breeze, It was made to be as simple as it is. jest can be used as a test runner with a separate library like chai to act as the test suite. react-testing-library which is used as an alongside jest as a test suite for unit testing React applications

A few notes:

  • Always use the matcher that is close to the behavior being tested eg. expect(true).toBe(true) instead of expect(true).toBeTruthy() the former is closer to the behavior being tested in the case compared to the latter though both would still pass.

NB: Add this to "types: tsconfig.config.ts" to enable the Jest TypeScript types:

npm i -D @types/jest
{
  "types": ["jest"] // enables `jest intellisense`
}

The above entry resolves this type warning: jest_error

  • Always ensure Promises and any asynchronous Promise rejections are handled else, you’ll get a Timeout of 5000ms exceeded error from jest
  • Always feel free to use extended matchers if current default matchers do not match the behavior being tested in your code.

Read more about Jest here and extended matchers here using matchers