Categories
Testing

Introduction to Testing with Jest

Spread the love

With apps being as complex as they are today, we need some way to verify that our changes to apps didn’t cause regressions. To do this, we need unit tests.

We can add unit tests to JavaScript apps easily with the Jest test runner. It’s very easy to get running and we can write lots of tests with it that runs quickly.

In this article, we’ll look at how to set up Jest from scratch and write some tests with it. Both synchronous an asynchronous functions will be tested.

Getting Started

To get started, we simply run a few commands. In our app’s project folder, we run:

yarn add --dev jest

or

npm install --save-dev jest

to add Jest to our JavaScript apps.

The example we have below will have 2 simple scripts each containing a module that has several functions.

First we create the code that we’re testing. We’ll test both code that does and doesn’t call APIs.

We create example.js and put in the following code:

const add = (a, b) => a + b;
const identity = a => a;
const deepCopy = (obj, copiedObj) => {
    if (!copiedObj) {
        copiedObj = {};
    }
    for (let prop of Object.keys(obj)) {
        copiedObj = {
            ...copiedObj,
            ...obj
        };
        if (typeof obj[prop] === 'object' && !copiedObj[prop]) {
            copiedObj = {
                ...copiedObj,
                ...obj[prop]
            };
            deepCopy(obj[prop], copiedObj);
        }
    }
    return copiedObj;
}
module.exports = {
    add,
    identity,
    deepCopy
}

The code above has functions to add numbers and a function that returns the same thing that it passes in.

Then we create request.js with the following:

const fetchJokes = async () => {
    const response = await fetch('[http://api.icndb.com/jokes/random/'](http://api.icndb.com/jokes/random/%27));
    return response.json();
};
module.exports = {
    fetchJokes
}

The code above gets random jokes from the Chuck Norris Jokes API.

Writing Tests for Synchronous Code

Now that we have the code that we want to test, we’re ready to create some test code for them.

First we create tests for the functions in example.js.

We add the following tests:

const { add, identity } = require('./example');

test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
});

test('adds 1 + 2 is truthy', () => {
    const sum = add(1, 2);
    expect(sum).toBeTruthy();
});

test('adds 1 + 2 to be defined', () => {
    const sum = add(1, 2);
    expect(sum).not.toBeUndefined();
    expect(sum).toBeDefined();
});

test('identity(null) to be falsy', () => {
    expect(identity(null)).toBeFalsy();
    expect(identity(null)).not.toBeTruthy();
});

The code above is very simple. It imports the functions from example.js and runs tests with it.

The first test calls add from example.js by passing in 2 numbers and checking that the returned result is what we expect.

The 2 tests below it are very similar except that we use different matcher functions to check the results.

The last test runs the identity function with null and they use the toBeFalsy and not.toBeTruthy matchers to check that null is indeed falsy.

In summary, Jest has the following matchers:

  • toBeNull — matches only null
  • toBeUndefined — matches only undefined
  • toBeDefined — is the opposite of toBeUndefined
  • toBeTruthy — matches anything truthy
  • toBeFalsy — matches anything falsy
  • toBeGreaterThan — check if a value is bigger than what we expect
  • toBeGreaterThanOrEqual — check if a value is bigger than or equal to what we expect
  • toBeLessThan — check if a value is less than what we expect
  • toBeLessThanOrEqual — check if a value is less than or equal what we expect
  • toBe — check if a value is the same using Object.is to compare the values
  • toEqual — checks every field recursively of 2 objects to see if they’re the same
  • toBeCloseTo — check for floating point equality of values.
  • toMatch — check if a string matches a give regex
  • toContain — check if an array has the given value
  • toThrow — check if an exception is thrown

The full list of expect methods are here.

We can use some of them as follows by adding them to example.test.js:

test('but there is a "foo" in foobar', () => {
    expect('foobar').toMatch(/foo/);
});

test(
    'deep copies an object and returns an object with the same properties and values',
    () => {
        const obj = {
            foo: {
                bar: {
                    bar: 1
                },
                a: 2
            }
        };
        expect(deepCopy(obj)).toEqual(obj);
    }
);

In the code above, we use the toMatch matcher to check if 'foobar' has the 'foo' substring in the first test.

In the second test, we run our deepCopy function that we added earlier and test if it actually copies the structure of an object recursively with the toEqual matcher.

toEqual is very handy since it checks for deep equality by inspecting everything in the object recursively rather than just for reference equality.

Writing Tests for Asynchronous and HTTP Request Code

Writing tests for HTTP tests requires some thinking. We want the tests to run anywhere with or without an internet connection. Also, the test shouldn’t depend on the results of the live server since it’s supposed to be self-contained.

This means that we have to mock the HTTP request rather than calling our code directly.

To test our fetchJokes function in request.js, we have to mock the function.

To do this, we create a __mocks__ folder in our project folder and in it, create requests.js. The file name should always match the filename with the code that we’re mocking.

We can mock it as follows:

const fetchJokes = async () => {
    const mockResponse = {
        "type": "success",
        "value": {
            "id": 407,
            "joke": "Chuck Norris originally wrote the first dictionary. The definition for each word is as follows - A swift roundhouse kick to the face.",
            "categories": [

]
        }
    };
    return Promise.resolve(mockResponse);
};
module.exports = {
    fetchJokes
}

Both the real and mock fetchJokes function returns a promise. The real function returns a promise from the fetch function while the mock one calls Promise.resolve directly.

This means that the real one actually makes the GET request and the mock tests resolve to the response that we want to test for.

Then in example.test.js, we add:

jest.mock('./request');
const { fetchJokes } = require('./request');

to the top of the file and:

test('jokes to be fetched', async () => {
    const mockResponse = {
        "type": "success",
        "value": {
            "id": 407,
            "joke": "Chuck Norris originally wrote the first dictionary. The definition for each word is as follows - A swift roundhouse kick to the face.",
            "categories": [

]
        }
    };
    await expect(fetchJokes()).resolves.toEqual(mockResponse)
});

to the bottom of it.

The test above just call the mock fetchJokes function since we have:

jest.mock('./request');

However, we still need to import the real one since the real one is replaced with the mock one when the test runs.

resolves will resolve the promise returned from the mock fetchJokes function and toEqual will check the structure of the response from the promise’s resolved value.

Finally, in the scripts section of package.json , we put:

"test": "jest"

so we can run npm test to run the tests.

Then when we run npm test , we should get:

PASS  ./example.test.js
  √ adds 1 + 2 to equal 3 (2ms)
  √ adds 1 + 2 is truthy
  √ adds 1 + 2 to be defined (1ms)
  √ identity(null) to be falsy
  √ but there is a "foo" in foobar
  √ deep copies an object and returns an object with the same properties and values (1ms)
  √ jokes to be fetched (1ms)

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        1.812s, estimated 2s
Ran all test suites.

No matter how and when we run the tests, they should still pass since they’re independent of each other and don’t rely on external network requests.

Conclusion

When we write unit tests, we have to make sure each test is independent from each other and also should make any network requests.

Jest makes testing easy by letting mock things like code that makes HTTP requests easily.

It’s also easy to set up, and has lots of matchers for all kinds of tests.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *