Why you should use jest-when to enhance your test mocks

Maud
OVRSEA
Published in
8 min readOct 4, 2022

TL;DR

jest-when is a powerful, easy-to-use library that allows to specify return values depending on the arguments passed to the mocked function.

Here is why you should use it:

  • it overcomes some limitations of jest’s native mock methods; notably, the need to specify the correct invocation order of the mocked function — which can be a nightmare in the case of asynchronous calls;
  • it allows to better decouple your tests from the implementation; notably by becoming independent of the invocation order of the mocked function;
  • it improves the readability of your tests by making them more concise and comprehensible;
  • mastering it requires a minimal investment of your time thanks to the similarity of its API to that of jest.

Here is what you have to do to use jest-when within a test:

// 1. Specify the function to mock
jest.mock("./path/to/functionToMockFile", () => ({
functionToMock: jest.fn(),
}));
// 2. Specify the return value based on the arguments received
when(functionToMock)
.calledWith(matchedArguments)
.mockReturnedValue(returnValue);
// NB: mockReturnedValue may be replaced by any jest method: mockResolvedValue, mockImplementation, mockResolvedValueOnce...

Mocking is a technique consisting of replacing dependencies with pre-programmed data, thereby allowing to isolate the test subject. In JavaScript, one of the most popular framework for tests is jest. Handily, jest comes natively with plenty of very handy mock methods that allow to intercept calls and set controlled return values. Consequently, its mock methods are widely used by the community.

However, in certain cases, these built-in methods scarcely manage to do the trick and can become delicate to handle without confusion.

In this article, we will go through an example to uncover such limitations of the native jest mock methods and show how jest-when, another JavaScript library complementing jest, can overcome some of these weaknesses.

Let’s begin with a simple example.

Say we want to compute the euclidian distance between two locations. As a reminder, the euclidian distance between two points p and q of cartesian coordinates (p1, p2) and (q1, q2) is:

This can be implemented as follows in a function named computeDistance:

Here, the computeDistance function takes two objects of type Location as arguments and calculates the euclidian distance between these two locations by :

  1. fetching the coordinates of the two locations via successive calls to the fetchCoordinates function to retrieve their coordinates;
  2. applying the euclidian distance formula to the fetched coordinates.

The implementation of the fetchCoordinates function is of no real interest in our case study. The only thing we need to know about this function is its interface: it takes a location parameter and returns its coordinates as drafted hereunder.

Standard way of mocking: jest native methods

If we want to make sure our computeDistance function is correctly implemented, we would probably write a test along the lines of:

However, given that fetchCoordinates encapsulates a call to an external API, you would most likely need to mock this function.

The function tested (computeEuclidianDistance) makes two distinct calls to fetchCoordinates and, to have the test pass, we need it to return two distinct values (first, the coordinates for Rome and second, those for Tirana). Luckily, there is a native jest mock method allowing to return a given resolved promise value at each different invocation: mockResolvedValueOnce() (or, equivalently, mockReturnValueOnce() in the case a synchronous function is being mocked).

In practice, prepending the following lines to the test above will suffice:

With this piece of code, the first time fetchCoordinates is invoked, it returns the coordinates of Rome (0, 0) and the second time it returns those of Tirana (0, 1).

If you need to run a second test with mocks returning different values in a different order, the following piece would work without any problem:

This is reasonably simple and allows tests to pass without any problem. Nonetheless, let’s keep in mind that this requires to set mocked values in the exact order in which the function is called.

Now, let’s spice things up a bit !

Let’s assume we want to find out which distance is the shortest between a list of possible locations and a reference point. In that case, we may define a function computeMinDistance that would look like this:

Here, computeMinDistance starts by computing all distances between each location of the input list on the one hand and the reference point on the other hand, and then selects the minimal distance among the lot.

If we wanted to find the minimal distance between a given reference point (say, Rome) and two other locations (say, Tirana and Berlin), here is the test that we would implement:

This code is straightforward, but there is one thing missing: we need to define the successive outputs that fetchCoordinates returns. In this case, the function is called 4 times:

  1. to fetch Rome coordinates in the Rome-Tirana distance computation;
  2. to fetch Tirana coordinates in the Rome-Tirana distance computation;
  3. to fetch Rome coordinates in the Rome-Berlin distance computation;
  4. to fetch Berlin coordinates in the Rome-Berlin distance computation.

Now, here is the tricky part: given our implementation, in which order are these four calls made ? Actually, this order depends on the speed at which each promise is processed by the backend side (see Appendix below for a proof of this result).

Therefore, in our test implementation, the order in which the mocked values are specified is tightly coupled to the implementation of the function that invokes the mocked one. As a consequence, even though native jest mock methods such as mockResolvedValueOnce(), mockReturnValueOnce() or mockImplementationOnce() are very handy to make mocks, they can become tricky to use when the invocation order is not crystal clear (as within Promise.all groups).

In addition, in such cases where the invocation order is a bit blurry, tests become much less readable and comprehensible for other developers reading the code (they would need to understand in depth in what order JavaScript processes each line of code).

Mock return values based on arguments received

To avoid the trouble of determining the exact order in which the mocked function should return each successive value, the easiest way would be to mock the returned values based on the arguments passed to the mocked function.

This is exactly what the jest-when library is made for.

Hereafter is what we would write to adapt our example using jest-when:

As you can see, the syntax for jest-when is pretty simple:

when(functionName)          // 1. Wrap your function with when()
.calledWith(arguments) // 2. Add the arguments to match
.mockResolvedValue(value) // 3. Specify the return value

Once this is done, anytime the mocked function is called with the passed arguments, it returns the specified value. As such, there is no more need to find out in which order the mocked function is called: the mere arguments it receives determine the return value.

Another advantage of jest-when is the similarity of its API with that of jest. As such, all usual jest behaviors and features are conserved in jest-when:

  • The scope of the mocked function is bounded to the block where it is defined. In other words, if you need to setup a default return value for given arguments, define it at the top of the test file; but if you need different return values with the same argument in two distinct tests, define your mock within the adequate describe or test block.
  • It supports most existing features of jest such as: chaining and replacement of mock trainings, support for single calls as well as resolved and rejected promises, resetting between tests, use of most jest matchers, etc…

You can find out all the existing features of this library on the jest-when documentation page.

Conclusion

In this article, we uncovered one limitation of jest’s native mock methods: namely, the need to specify mocked return values in the correct order, which can be tricky notably in the case of promises resolved concurrently.

Next, we have shown that jest-when, a lightweight JavaScript library complementing jest, succeeds in overcoming this limitation by matching mocked returned values with specific call arguments. Using this library to mock your dependencies also makes your tests tremendously more comprehensible, robust and modulable. Last, given the similarity of its API with that of jest, you will need to invest a very negligible amount of your time to learn to use it. All in all thus, jest-when is a great library to use to enhance your test mocks ; now you don’t have any reason not to be using it anymore !

Appendix — Proof that the order in which promises are fulfilled is not preserved on the backend side

In this appendix, we will bring proof that the order in which promises are fulfilled is *not* preserved on the back-end side. Let’s come back to our computeMinDistance function which can be tested with the following piece of code:

Now, let’s define how fetchCoordinates should be mocked. We thus need to define the successive outputs that fetchCoordinates returns. In this case, the function is called 4 times:

  1. to fetch Rome coordinates in the Rome-Tirana distance computation;
  2. to fetch Tirana coordinates in the Rome-Tirana distance computation;
  3. to fetch Rome coordinates in the Rome-Berlin distance computation;
  4. to fetch Berlin coordinates in the Rome-Berlin distance computation.

Given our implementation, in which order are these four calls made ?

Calls 1 and 2 on the one hand (Rome-Tirana distance computation), and 3 and 4 on the other hand (Rome-Berlin distance computation) are made sequentially within the computeDistance calls. Hence, these calls are necessarily made in the following order:

  • 1<2 on the one hand (read ‘<’ as ‘precedes’ or ‘is called before’);
  • 3<4 on the other hand.

Since the two invocations of the computeDistance function are grouped within a Promise.all call, they are run concurrently, from which ensues that the invocation order should be: 1~3 < 2~4.

In this case, we would thus need to set the mocks in the following order:

However, this order depends on the speed at which each promise is processed by the backend side. Indeed, if a more complex implementation ensued a longer time to execute computeDistance with the first arguments (Rome/Tirana) than the second ones (Rome/Berlin), the correct mock order would be different.

We can prove this simply by creating a timeout wrapper (hereunder named slowDown) and modify the computeMinDistance function so as to wrap it around the first execution of computeDistance. Hereunder are these modifications:

In this case, if we keep the mocks defined in the order previously set (1❤<2<4), the test fails because fetchCoordinates are invoked in a different order here. Indeed, because of the lag of 100ms preceding the first computeDistance call (with Rome/Tirana locations), the second computeDistance call (with Rome/Berlin locations) is invoked first, thus modifying the required mock order to: 3<4<1<2.

With the mocks defined in this order, the test finally passes.

All in all, thus, the order in which the promises are received by the backend is *not* preserved as they may be processed at different speed (this is in stark contrast with the behavior on the the client-side where the order is maintained — since the order of the resolved elements in the resulting array matches the order of the promises passed to Promise.all).

--

--

Maud
OVRSEA
0 Followers
Editor for

Software engineer at OVRSEA