Categories
Testing

How Do We Write Unit Tests to Test Our Code?

If we want to change our code without worry about breaking anything, then having unit tests is important.

In this article, we’ll look at how to write unit tests so that we can have peace of mind when changing code.

Writing Unit Tests

Unit tests shouldn’t be in a far-away corner of the source tree. It should be conveniently located so that we can easily look for them and change them is necessary.

For small projects, we can embed the tests in the module itself.

For larger projects, we can move them into their own subdirectory.

By writing unit tests, we’re providing developers who look at our code with 2 invaluable resources.

They include examples of how to use the functionality of our module. And a means to build regression tests to validate future change of our code.

Therefore. it’s great for documentation of our code and also makes updating the tests later easier for everyone.

A unit test will call a function with an argument and check against the results returned.

We must run them often so we’re checking if our stuff is still working all the time.

Using Test Harnesses

Test harnesses are standard sets of data and code that we use to run our tests.

It’s used to handle common things like logging, analyzing expected results, and selecting and running tests.

Test runners probably have lots of these functionalities of already so we can just use them.

However, if they aren’t present in our testing framework, then we’ve to write them ourselves.

We can create a base class that provides these common operations. Individual tests can be a subclass of the hardness class so that we can just call the methods in the test class.

This is good because it meets the DRY principle. We aren’t repeating any code for logging and checking results.

A harness should provide a standard way of setting up our tests and cleaning up after it’s run.

It should have a method for selecting individual tests or all available tests.

Also, it should have a means of analyzing output from expected or unexpected results.

Failure reporting should also be standard in our test harness.

Tests should be composable. This means that a test can be composed of subtests of subcomponents of any depth.

We may add tests to help us debug our code. This may be a logging statement or a code that interacts with the IDE.

Once we’re done debugging, then we formalize the test. If we break it once then it’s likely to break again, so we should just add a test to make sure that it doesn’t break again.

Build a Test Window

We should test our software once it’s been deployed with real-world data flowing through it,

We can do that with our own end-to-end tests. There’re many ways to make our app run like how a user would have interacted with it.

First, we need clean seed data populated inside our app. Then we need to write tests with frameworks like Selenium to interact with our app like how users would have interacted with it.

Once we’re done with the tests, we reset the data to the original seed data so that our tests will run consistently.

A Culture of Testing

All software that we write will tested by someone including our test and users. Therefore, we should make sure that we have to test it thoroughly before we release it into any environment.

This minimizes maintenance costs and customer service calls.

Therefore, testing should be a habit. If we don’t test our stuff, then our users will.

Conclusion

We should have a culture of testing ingrained. Because if we don’t test our stuff thoroughly then our users will.

To make our lives easier, we should write unit tests and end to end tests.

This way, we can test everything automatically without thinking once the test is written.

Also. we should make sure that we use a test harness or write our own so that we can run our tests and log the results of the tests.

Categories
Express Nodejs Testing

Add Tests to Express Apps With Jest and SuperTest

Automated tests are essential to the apps we write since modern apps have so many moving parts.

In this piece, we’ll look at how to write apps to test an Express app that interacts with a database with Jest and SuperTest.


Creating the App We’ll Test

We create a project folder by creating an empty folder and running the following to create a package.json file with the default answers:

npm init -y

Then we run the following to install the packages for our apps:

npm i express sqlite3 body-parser

Then, we create the app.js file for our app and write:

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');
const app = express();
const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;
let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

db.serialize(() => {
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
});

app.use(bodyParser.json());
app.get('/', (req, res) => {
    db.serialize(() => {
        db.all('SELECT * FROM persons', [], (err, rows) => {
            res.json(rows);
        });
    })
})

app.post('/', (req, res) => {
    const { name, age } = req.body;
    db.serialize(() => {
        const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');
        stmt.run(name, age);
        stmt.finalize();
        res.json(req.body);
    })
})

app.put('/:id', (req, res) => {
    const { name, age } = req.body;
    const { id } = req.params;
    db.serialize(() => {
        const stmt = db.prepare('UPDATE persons SET name = ?, age = ? WHERE id = ?');
        stmt.run(name, age, id);
        stmt.finalize();
        res.json(req.body);
    })
})

app.delete('/:id', (req, res) => {
    const { id } = req.params;
    db.serialize(() => {
        const stmt = db.prepare('DELETE FROM persons WHERE id = ?');
        stmt.run(id);
        stmt.finalize();
        res.json(req.body);
    })
})

const server = app.listen(port);
module.exports = { app, server };

The code above has the app we’ll test.

To make our app easier to test, we have:

const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;
let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

So we can set the process.env.NODE_ENV to 'test' to make our app listen to a different port than it does when the app is running in a nontest environment.

We’ll use the 'test' environment to run our tests.

Likewise, we want our app to use a different database when running unit tests than when we aren’t running them.

This is why we have:

let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

We specified that when the app is running in a 'test' environment we want to use SQLite’s in-memory database rather than a database file.


Writing the Tests

Initialization the code

With the app made to be testable, we can add tests to it.

We’ll use the Jest test runner and SuperTest to make requests to our routes in our tests. To add Jest and SuperTest, we run:

npm i jest supertest

Then, we add app.test.js to the same folder as the app.js file we had above.

In app.test.js, we start by writing the following:

const { app } = require('./app');
const sqlite3 = require('sqlite3').verbose();
const request = require('supertest');
const db = new sqlite3.Database(':memory:');

beforeAll(() => {
    process.env.NODE_ENV = 'test';
})

In the code above, we included our Express app from our app.js. Then we also included the SQLite3 and SuperTest packages.,

Then, we connected to our in-memory database with:

const db = new sqlite3.Database(':memory:');

Next, we set all the tests to run in the 'test' environment by running:

beforeAll(() => {
    process.env.NODE_ENV = 'test';
})

This will make sure we use port 3001 and the in-memory database we specified in app.js for each test.

To make our tests run independently and with consistent results, we have to clean our database and insert fresh data every time.

To do this, we create a function we call on each test:

const seedDb = db => {
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
    db.run('DELETE FROM persons');
    const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');
    stmt.run('Jane', 1);
    stmt.finalize();
}

The code above creates the persons table if it doesn’t exist and deletes everything from there afterward.

Then we insert a new value in there to have some starting data.


Adding Tests

With the initialization code complete, we can write the tests.

GET request test

First, we write a test to get the existing seed data from the database with a GET request.

We do this by writing:

test('get persons', () => {
    db.serialize(async () => {
        seedDb(db);
        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

We put everything inside the callback of db.serialize so the queries will be run sequentially.

First, we call seedDb, which we created above to create the table if it doesn’t exist, to clear out the database, and to add new data.

Then, we call the GET request by writing:

await request(app).get('/');

This gets us the res object with the response resolved from the promise.

request(app) will start the Express app so we can make the request.

Next, we have the response for us to check against for correctness:

const response = [
  { name: 'Jane', id: 1, age: 1 }
]

Then, we check the responses to see if we get what we expect:

expect(res.status).toBe(200);
expect(res.body).toEqual(response);

The toBe method checks for shallow equality, and toEqual checks for deep equality. So we use toEqual to check if the whole object structure is the same.

res.status checks the status code returned from the server, and res.body has the response body.

POST request test

Next, we add a test for the POST request. It’s similar to the GET request test.

We write the following code:

test('add person', () => {
    db.serialize(async () => {
        seedDb(db);
        await request(app)
            .post('/')
            .send({ name: 'Joe', age: 2 });

        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 },
            { name: 'Joe', id: 2, age: 2 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

First, we reset the database with:

seedDb(db);

We made our POST request with:

await request(app)
  .post('/')
  .send({ name: 'Joe', age: 2 });

This will insert a new entry into the in-memory database.

Finally, to check for correctness, we make the GET request — like in our first test — and check if both entries are returned:

const res = await request(app).get('/');
const response = [
  { name: 'Jane', id: 1, age: 1 },
  { name: 'Joe', id: 2, age: 2 }
]
expect(res.status).toBe(200);
expect(res.body).toEqual(response);

PUT and DELETE tests

The test for the PUT request is similar to the POST request. We reset the database, make the PUT request with our payload, and then make the GET request to get the returned data, as follows:

test('update person', () => {
    db.serialize(async () => {
        seedDb(db);
        await request(app)
            .put('/1')
            .send({ name: 'Joe', age: 2 });

        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

Then we can replace the PUT request with the DELETE request and test the DELETE request:

test('delete person', () => {
    db.serialize(async () => {
        seedDb(db);
        const res = await request(app).delete('/1');
        const response = [];
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

Running the Tests

To run the tests, we add the following to the scripts section:

"test": "jest --forceExit"

We have to add the --forceExit option so Jest will exist after the tests are run. There’s no fix for the issue where Jest tests using SuperTest don’t exit properly yet.

Then we run the following to run the tests:

npm test

And we should get:

PASS  ./app.test.js
  √ get persons (11ms)
  √ add person (2ms)
  √ update person (2ms)
  √ delete person (6ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.559s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

We should get the same thing no matter how many times we run the tests since we reset the database and made all database queries run sequentially.

Also, we used a different database and port for our tests than other environments, so the data should be clean.


Conclusion

We can add tests run with the Jest test runner. To do this, we have to have a different port and database for running the tests. Then we create the tables if they don’t already exist, clear all the data, and add seed data so we have the same database structure and content for every test.

With SuperTest, we can run the Express app automatically and make the request we want. Then, we can check the output.

Categories
Testing

Running Repetitive Test Code with Jest

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 .

Categories
Testing

Introduction to Testing with Jest

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.

Categories
Testing

Creating Automatic Jest Mocks

To create unit tests, we often have to mock functions to bypass various parts of the code. For example, if we don’t want to run code that calls an API in a function, then we have to create a mock of that function.

In this article, we’ll look at some ways to create mocks functions and classes in Jest tests.

ES6 Class Mocks

We can use Jest to create ES6 class mocks.

ES6 classes are just syntactic sugar on top of constructor functions, so we can use the same methods for mock functions to mock classes.

Making Something to Test

First, we make something to test by creating videoPlayer.js and add:

class VideoPlayer {
    constructor() {

    }

    playVideoFile(fileName) {
        console.log(`Playing ${fileName}`);
    }
}

module.exports = VideoPlayer;

Then we create app.js as follows:

const VideoPlayer = require('./videoPlayer');

class VideoPlayerApp {
    constructor() {
        this.videoPlayer = new VideoPlayer();
    }

    play() {
        const videoFileName = 'video.mp4';
        this.videoPlayer.playVideoFile(videoFileName);
    }
}

module.exports = VideoPlayerApp;

Creating Automatic Mocks

In this example, we’ll mock the VideoPlayer class.

To do this, we can call jest.mock(‘./videoPlayer’); since we export the VideoPlayer class with module.exports .

Jest is smart enough to create its own mock class in place of the actual VideoPlayer class in tests.

It’ll replace the class with a mock constructor function, and replaces all its methods with mock functions that always return undefined .

Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls, where theAutomaticMock is replaced with the same name as the class we originally mocked.

In our example, we aren’t going to create our own implementation of the methods, so once we called jest.mock(‘./videoPlayer’); , we’re done with mocking the VideoPlayer class.

Then we can create app.test.js to put our tests. We add the following:

const VideoPlayer = require('./videoPlayer');
const VideoPlayerApp = require('./app');
jest.mock('./videoPlayer');

beforeEach(() => {
    VideoPlayer.mockClear();
});

test('VideoPlayer is called once', () => {
    const videoPlayerApp = new VideoPlayerApp();
    expect(VideoPlayer).toHaveBeenCalledTimes(1);
});

test('VideoPlayer is called with video.mp4', () => {
    expect(VideoPlayer).not.toHaveBeenCalled();
    const videoPlayerApp = new VideoPlayerApp();
    videoPlayerApp.play();
    const videoFileName = 'video.mp4';
    const mockVideoPlayer = VideoPlayer.mock.instances[0];
    const mockPlayVideoFile = mockVideoPlayer.playVideoFile;
    expect(mockPlayVideoFile.mock.calls[0][0]).toEqual(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledWith(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledTimes(1);
});

We important the modules for containing each class. The VideoPlayer class will be replaced with our mock, but we still have to import it.

Then we have our beforeEach hook which runs:

VideoPlayer.mockClear();

to clear the mocks of the VideoPlayer class.

Next, we write our first test, which is:

test('VideoPlayer is called once', () => {
    const videoPlayerApp = new VideoPlayerApp();
    expect(VideoPlayer).toHaveBeenCalledTimes(1);
}););

We create a new VideoPlayerApp instance. The constructor of the VideoPlayerApp calls runs new VideoPlayer(); , which means the VideoPlayer class is called.

Then we check that:

expect(VideoPlayer).toHaveBeenCalledTimes(1);

which should be true since the VideoPlayer constructor function, aka the class, is called once.

Next, we look at our second test, which is more complex:

test('VideoPlayer is called with video.mp4', () => {
    expect(VideoPlayer).not.toHaveBeenCalled();
    const videoPlayerApp = new VideoPlayerApp();
    videoPlayerApp.play();
    const videoFileName = 'video.mp4';
    const mockVideoPlayer = VideoPlayer.mock.instances[0];
    const mockPlayVideoFile = mockVideoPlayer.playVideoFile;
    expect(mockPlayVideoFile.mock.calls[0][0]).toEqual(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledWith(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledTimes(1);
});

First, we check that VideoPlayer hasn’t been called to make sure that VideoPlayer.mockClear() is working.

Then we create a new instance of VideoPlayerApp and call play on it as follows:

const videoPlayerApp = new VideoPlayerApp();
videoPlayerApp.play();

After that, we get our mock VideoPlayer instance by running:

const mockVideoPlayer = VideoPlayer.mock.instances[0];

Then we call the playVideoFile method of the VideoPlayer class that was mocked as follows:

const mockPlayVideoFile = mockVideoPlayer.playVideoFile;

Then we can get the first argument that’s passed into the mock playVideoFile call by using:

mockPlayVideoFile.mock.calls[0][0]

We can then check that the call was done with 'video.mp4' passed by writing:

expect(mockPlayVideoFile.mock.calls[0][0]).toEqual(videoFileName);

Equivalently, we can write:

expect(mockPlayVideoFile).toHaveBeenCalledWith(videoFileName);

Finally, we check that the playVideoFile method was called only once by running:

expect(mockPlayVideoFile).toHaveBeenCalledTimes(1);

This makes sense since playVideoFile is only called once in our test.

Photo by Andrew Tanglao on Unsplash

Running the Tests

Be sure to install Jest by running:

npm i jest

and put:

"test": "jest"

in the scripts section so we can run the tests.

In the end, we should get:

PASS  ./app.test.js
  √ VideoPlayer is called once (3ms)
  √ VideoPlayer is called with video.mp4 (2ms)

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

Conclusion

With Jest’s automatic mocks, we can mock classes or constructor functions easily.

All methods are mocked with functions that return undefined.

Then we can retrieve the mock by using mockedObject.mock.instances, which is an array.

Then we can run checks as usual. with toEqual, toHaveBeenCalledWith, and other matcher methods.