Unit test coverage grants confidence that code logic is correct(and serves as great developer documentation!). Test design can benefit from the mocking of both inconsequential, long-running processes and interactions with state external to the code under test. In addition, this mocking can help limit the scope of a test, allowing for better targeting of incorrectness. The jest testing framework provides powerful dependency mock tooling that makes this work trivial.

This post was peer reviewed by Kevin Hosford

The jest test framework has a simple dependency mocking API that leverages the Node.js module system as a test-runtime, dependency injection system. Any dependencies imported in a Javascript file via require statements can have their default behavior overridden(Note: jest can be configured to properly parse ES module syntax as well)

A mock’s behavior can be activated (deactivated by default) on a per test script basis. For example, a test script user.spec.js could opt into mocked behavior of the native Node.js fs library while account.spec.js could prefer to use the actual fs logic provided by the Node.js native library. It’s worth noting that jest doesn’t allow both on the same test script: a test script cannot mock the module for some tests and then revert to a dependency’s actual logic for other tests. At the test script level it’s one or the other.

jest uses a simple convention to locate the behavior definitions for manual mocks. It expects a JavaScript file with the same name as the dependency to be mocked, located in a __mocks__ subdirectory of the path hosting said dependency. Native Node.js libraries (e.g. fs ) and dependencies that have been npm installed have a slightly different convention: the mock definition file should be named the same as the string used in the require(e.g. fs would be mocked by fs.js), located in a __mocks__ subdirectory off the project’s root directory.

Consider the authenticateUser export from the following Node.js module:

Writing unit tests against this example module, one would probably want to mock both the bcrypt and the getHashedPasswordFromDB imports. bcrypt methods should be mocked because they can take time to execute. getHashedPasswordFromDB methods should be mocked to avoid unwanted database queries. Also, one will want to write tests to assert that authenticateUser can handle cases where either bcrypt or getHashedPasswordFromDB throw errors. Manual mocks of these dependencies enables one to emulate these error cases deterministically.

As an example, let’s mock getHashedPasswordFromDB. We’ll need to create a mock definition file associated with ./models/user.js . This will be /models/__mocks__/user.js:

The idea here is that the public interface of the dependency to be mocked should be recreated with alternative implementations that have similar return behavior. In this case, our function under test, authenticateUser, expects to be able to import a function getHashedPasswordFromDB from ./models/user.js that returns a Promise. We use jest.fn() to create a Jest mock object which will serve as the export. The behavior of returning a Promise can be emulated by passing an anonymous function to jest.fn() that defines this behavior. See the next section for more on this.

The behaviors defined in these mock scripts can be thought of as the default behaviors of the mock. A lot of times, these default behaviors will need to be overridden in favor of more unique behavior specific to particular tests. We’ll look at an example of this in a bit.

For the mocked function getHashedPasswordFromDB, we defined the default behavior to be the returning of a Promise that resolves with an empty object. Defining sophisticated mock behaviors is made trivial by leveraging the jest object’s test spy factory method: fn.

Invoked without any arguments, jest.fn()returns the basic, "no-op” jest spy object. When this spy object is invoked, it returns undefined(i.e. same as invoking function () {}). This can prove effective at preventing some types of dependency invocations from generating unwanted side effects e.g. preventing invocations of logger library methods from generating logs during test execution.

If the factory is passed a function, the returned spy object will behave as the passed function on invocation e.g. when the return of

jest.fn(function increment(value) { return value + 1 })

is invoked, the observed behavior will mimic

function increment(value) { return value + 1 }

The test spy generated via jest.fn() are incredibly powerful. Consult the jest test spy API documentation for all the details.

Let’s write a test that asserts:

> if getHashedPasswordFromDB throws an error, authenticateUser returns a Promise that rejects with said error

As written, this test engages a manual mock of the module user.js (line 5). Next, the mock’s getHashedPasswordFromDB function has it’s default mock behavior overridden(line 14) with a special behavior unique to this test: it will throw a particular error. This is accomplished with the test spy’s mockImplementationOnce() method which will override the mock’s default behavior for a single invocation, afterwords reverting to the default mock behavior.

If the mockImplementationOnce() method had not been invoked on line 14, this test would have used the default behavior defined in the manual mock of getHashedPasswordFromDB.

As shown, the jest test framework provides great dependency mocking tools to make unit tests more efficient and deterministic. The jest project is constantly growing, introducing more convenient features at a regular pace. This is great for those writing unit tests against both front and back end javascript code: you’ll have a battle-tested, test framework with great community support to cover most test case one can expect to encounter. Unfortunately, all of this power and convenience might spoil the developer, leading one wanting similarly simple tools for testing in other programming languages :D

A journaling of solutions to interesting problems encountered in the modern web stack @henryslama www.dslama.net