Introduction to Unit Testing with Jest

Why Test? Why Jest?

Writing tests gives us the ability to build applications that are robust and efficient. It ensures that we build applications that are easily maintainable over long periods of time while writing clean code. It provides a way to probe different parts of the application, to find possible bugs which could occur now or in the future.

Jest provides an easy and intuitive way to write simple but robust tests for our applications.

Installation

First, we will install jest in our javascript application:

Using npm

npm i --save-dev jest

Using yarn

yarn add --dev jest

Configuration

We will open our package.json file and under scripts, we will add Jest

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

This will enable npm run test to trigger our tests to be run

Writing Test

We now need to create files with the ending test.js which is where we will be writing our test. For example, we want to test a function that squares a number, our test file will be named squares.test.js.

Jest allows us to import functions that we want to test. We can test both synchronous and asynchronous functions. Our squares.test.js would look like this:

import square from squares.js

describe('Squares Function', () => {
    test('Square of 2 is equal to 4', () => {
         expect(square(2)).toBe(4)
    })
})

We do not need to import describe or test, Jest refers to these as globals. They are accessible from anywhere in our test file. You can see the full list of globals here

Let's break our first test down...

Describe()

describe() helps us group related tests, this improves the readability of our tests. It is not necessary but it ensures we keep our tests organized especially when writing multiple tests. A good instance is if we are writing multiple tests for the same function.

describe('Squares Function', () => {
    test('Square of 2 is equal to 4', () => {
         expect(square(2)).toBe(4)
    })
    test('Square of 3 is equal to 9', () => {
         expect(square(3)).toBe(9)
    })
})

Test()

test() is where we write the actual test. It takes two parameters, the name or description of the test and a function to run the test. It can take a timeout as an optional third parameter, this is very useful for testing asynchronous functions which we will discuss later in this article.

Expect()

expect() is where we pass our function to be tested. .toBe() is one of the matchers in Jest and it helps us to check for equality. When expect() calls square(2) the toBe() matcher will check if the returned value is equal to 4. If it is not equal to 4, then the test will fail.

Matchers

Matchers help us compare the value our functions return to the one the test expects. Matchers can test equality, truthiness, and a lot more. Let us explore a few of the common matchers in Jest.

//Negating a test
test('Square of 2 is not Equal to 6', () => {
  expect(square(2)).not.toBe(6)  
})

//Testing numbers
test('Square of 2', () => {
  expect(square(2)).toBeLessThanOrEqual(5);
  expect(square(2)).toBeGreaterThanOrEqual(3);
  expect(square(2)).toBeLessThan(6);
})

//Checking arrays
const fruits = [
  'apple',
  'banana',
  'orange',
  'grape'
];

test('fruits array has orange in it', () => {
  expect(shoppingList).toContain('orange');
});

The full list of matchers can be found in the official documentation.

Handling Asynchronous Functions

There will be scenarios where we have to test asynchronous functions. We will have to let Jest know which function returns a promise. This will ensure that the test is not executed before the promise is returned.

Still using our squares function, assuming it is an asynchronous piece of logic, then, we will write our test this way:

//Promises
test('Square of 2 is equal to 4', () => {
    return square(2).then(result => {
        expect(result).toBe(4)
    }
})

//Async/Await
test('Square of 2 is equal to 4', async() => {
    let data = await square(2)
    expect(data).toBe(4)
})

//Or

test('Square of 4 is not equal to 12', async() => {
      await expect(square(4)).resolves.not.toBe(12)
})

Writing tests for our applications will help us to build loosely coupled and reusable functions that can be tested. We should also remember that it makes our code less prone to bugs and easily maintainable in the long term.

Happy Testing! Cheers!