Categories
Testing

Running Repetitive Test Code with Jest

Spread the love

In our unit tests, we often have to write test code that runs before and after each test. We’ve to write them often to run code to set up fixtures before tests and run code to clean up everything after tests.

We can easily do this with Jest since it comes with a few hooks to do this.

In this article, we’ll look at how to write repetitive setup and teardown code in a way that isn’t repetitive.

How to Write Setup and Teardown Code

With Jest, we can write setup and teardown code by using the beforeEach and afterEach hooks.

beforeEach runs code before each test, and afterEach runs code after each test. The order applies inside a describe block and if there’s no describe block, for the whole file.

For example, given that we have the following code in example.js :

const getStorage = (key) => localStorage.getItem(key);
const setStorage = (key, value) => localStorage.setItem(key, value);
module.exports = {
    getStorage,
    setStorage
}

We write the test for them as follows:

const { getStorage, setStorage } = require('./example');

beforeEach(() => {
    localStorage.setItem('foo', 1);
    expect(localStorage.getItem('bar')).toBeNull();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

We pass a callback function into the beforeEach hook to run code before each test that we have below.

In this example, we prepopulate local storage with an item with key 'foo' and value '1' .

We also check that local storage doesn’t have the item with key 'bar’ with:

expect(localStorage.getItem('bar')).toBeNull();

All tests will pass with the expect we have above since we’ll run localStorage.clear() in the afterEach hook.

Likewise, we pass in a callback to afterEach to run code after each test is run.

We clear local storage with:

localStorage.clear();

Then when running the tests, we get that item with key'bar' is null since we didn’t populate it in the first test.

In the second test, we’ll get that expect(getStorage(‘foo’)).toBe(‘1’); passing since we populated the local storage with it in our beforeEach hook.

Then since we ran:

setStorage('bar', 2);

to to save an item with key 'bar' and value '2' , we’ll get that:

expect(bar).toBe(2);

passing since we saved the item in the test.

Asynchronous Code

In the example above, we ran synchronous code in our hooks. We can also run asynchronous code in our hooks if they either take a done parameter or return a promise.

If we want to run a function that takes a callback and runs it asynchronously as follows:

const asyncFn = (callback) => {
    setTimeout(callback, 500);
}

Then in the beforeEach hook we can run asyncFn as follows:

const { asyncFn } = require('./example');

beforeEach((done) => {
    const callback = () => {
        localStorage.setItem('foo', 1);
        done();
    }
    asyncFn(callback);
    expect(localStorage.getItem('bar')).toBeNull();
});

It does the same thing as the previous beforeEach callback except it’s done asynchronously. Note that we call done passed in from the parameter in our callback.

If we omit, it, the tests will fail as they time out. The code in the tests is the same as before.

We can wait for promises to resolve by returning it in the callback we pass into the hooks.

For example, in example.js , we can write the following function to run a promise:

const promiseFn = () => {
    return new Promise((resolve) => {
        localStorage.setItem('foo', 1);
        resolve();
    });
}

Then we can put promiseFn in module.exports and then run it in our beforeEach by running:

beforeEach(() => {
    expect(localStorage.getItem('bar')).toBeNull();
    return promiseFn();
});

We ran promiseFn which we imported before this hook and we return the promise returned by that function, which sets the local storage like the first example except it’s done asynchronously.

Then after we put everything together, we have the following code in example.js , which we run in our test code in the hooks and for testing:

const getStorage = (key) => localStorage.getItem(key);
const setStorage = (key, value) => localStorage.setItem(key, value);
const asyncFn = (callback) => {
    setTimeout(callback, 500);
}
const promiseFn = () => {
    return new Promise((resolve) => {
        localStorage.setItem('foo', 1);
        resolve();
    });
}
module.exports = {
    getStorage,
    setStorage,
    asyncFn,
    promiseFn
}

Then we have asyncExample.test.js to change the hook to be asynchronous:

const { getStorage, setStorage, asyncFn } = require('./example');

beforeEach((done) => {
    const callback = () => {
        localStorage.setItem('foo', 1);
        done();
    }
    asyncFn(callback);
    expect(localStorage.getItem('bar')).toBeNull();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

Then in example.test.js we have:

const { getStorage, setStorage } = require('./example');

beforeEach(() => {
    localStorage.setItem('foo', 1);
    expect(localStorage.getItem('bar')).toBeNull();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

and finally in promiseExample.test.js we have:

const { getStorage, setStorage, promiseFn } = require('./example');

beforeEach(() => {
    expect(localStorage.getItem('bar')).toBeNull();
    return promiseFn();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

BeforeAll and AfterAll

To only run the setup and teardown code once in each file, we can use the beforeAll and afterAll hooks. We can pass in a callback with the code that we want to run like we did with the beforeEach and afterEach hooks.

The only difference is that the callbacks are run once before the test in a file is run and after the test in a file is run instead of running them before and after each test.

Callback of beforeAll runs after the callback for beforeEach and callback for afterAll runs after the callback forafterEach .

The order applies inside a describe block and if there’s no describe block, for the whole file.

Running Only One Test

We can write test.only instead of test to run only one test. This is handy for troubleshooting since we don’t have to run all the tests and it helps us pin issues with the test that’s failing.

To write test code that needs to be run for all tests, we use the beforeEach and afterEach hooks in Jest.

To write test code that’s only run per describe block or file, we can use the beforeAll and afterAll hooks.

Callback of beforeAll runs after the callback for beforeEach and callback for afterAll runs after the callback forafterEach .

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 *