When it comes to frontend testing, most developers think of slow tests using selenium and a connected browser in an error prone setup. With more and more logic moving to the frontend, this picture is now slowly changing. Modern javascript testing frameworks make it possible to run tests fast and reliably.
Among them, the Jest framework is one of the more mature ones. The focus is on making it easy to use, which means that it should be easy to setup, fast to run tests, and should have all the features a developer expects from a modern testing framework. We successfully use it at TOPdesk for our frontends in a setup with Vue.js and Webpack.
To set the mood, here is a really simple example of a Jest test.
describe('First testsuite', () => { test('Simple test', () => { expect(2 + 3).toBe(5); }); });
The code looks really similar to other js testing frameworks. Test suites are defined with the describe function and tests with the test function. A test suite can contain several tests or even other test suites. Tests can be run using nodejs or from the IDE. IntelliJ even supports debugging a test.
Jest is a very feature rich framework. It comes with built in assertions, mocks, test lifecycle hooks, parameterized tests, and much more. In this article, I want to focus on the provided assertion methods and how to use them.
Assertions
Jest comes with enough assertions built in to satisfy most needs. Besides assertions for equality, there are assertion methods for numbers, arrays, objects and convenience methods to test for common values like undefined.
Assertions are expressed using fluent syntax. That means that methods are named and chained in a manner that it reads like prose.
Every assertion starts with ‘expect’ followed by an assertion method, like toBe, toBeGreaterThan and so on.
An assertion can be negated by inserting ‘not’ into the chain:
expect(1).not.toBe(2)
Some assertions can also be nested which is useful when asserting the structure of arrays or objects (see below for details):
expect([{a: 1}, {b: 2}]).toEqual(expect.arrayContaining([{a: 1} ]))
Testing equality
As always in Javascript, when you want to test for equality, you have to ask yourself “Which kind of equality?”. This is reflected by several equality assertion methods in Jest: toBe, toEqual and toStrictEqual.
toBe compares the referential identity of values, while toEqual does a deep comparison of the properties of the values (using Object.is). toEqual is therefore better suited for objects, unless it is really crucial that an object is the same instance. toStrictEqual goes one step further than toEqual and also checks that two objects have the same type.
The following code snippet illustrates the behaviour:
test('To be or to equal', () => { const obj = { a: 2 }; expect(obj).toBe(obj); expect({ a: 2 }).not.toBe({ a: 2 }); expect({ a: 2 }).toEqual({ a: 2 }); expect({ a: 2 }).not.toEqual({ b: 3}); expect({ a: 2 }).toStrictEqual({ a: 2 }); expect({ a: 2 }).toEqual({ a: 2, b: undefined }); expect({ a: 2 }) .not.toStrictEqual({ a: 2, b: undefined }); });
Partial matches
Arrays and objects
You can use the equal assertion methods to compare arrays and objects, however, when writing tests for arrays and objects, you often only want to check parts of the returned value. For example, that a certain item is in the list, or that the object has a certain state or property. Jest provides some functions to accommodate testcases like this.
You can use the toContain and toContainEqual functions to check that an array contains a certain item. The difference in these two functions is the equality comparison, which is analogous to toBe and toEqual.
expect.arrayContaining goes one step further and lets you test if an array contains a certain subset of elements (comparing individual elements with deep equality). The method is a bit different than the assertion methods mentioned so far, because it cannot stand on its own, but needs to be coupled with another assertion. This makes it very flexible to use, as it can be combined with different assertions. In the following code snippet it is combined with toEqual:
test('Array methods', () => { expect([1, 2, 3]).toContain(2); expect([{a: 1}, {b: 2}]).not.toContain({b: 2}); expect([{a: 1}, {b: 2}]).toContainEqual({b: 2}); expect([1, 2, 3]) .toEqual(expect.arrayContaining([1, 2])); });
Analogous to that, expect.objectContaining checks if an object contains a subset of properties. The toMatchObject function looks very similar to that on the first glance. The two functions indeed behave the same way when applied to objects.
However, when applied to arrays, expect.objectContaining behaves the same way as toEqual (deep level object comparison), while toMatchObject goes its own way: It checks that the array contains the exact number of elements and that each element contains a subset of properties of the received element at the same position. In that it differs from expect.arrayContaining which allows to test for a subset of elements in the array, but not for a subset of properties of individual elements.
test('toMatchObject vs. objectContaining', () => { expect({a: 1, b: 2}).toMatchObject({a: 1}); expect({a: 1, b: 2}) .toEqual(expect.objectContaining({a: 1})); expect([{a: 1, b: 2}]).toMatchObject([{a: 1}]); expect([{a: 1, b: 2}]) .not.toEqual(expect.objectContaining([{a: 1}])); }); test('toMatchObject vs. arrayContaining', () => { expect([{a: 1}, {b: 2, c: 3}]) .toMatchObject([{a: 1}, {b: 2}]); expect([{a: 1}, {b: 2, c: 3}]) .not.toEqual(expect.arrayContaining([{a: 1}, {b: 2}])); expect([{a: 1}, {b: 2, c: 3}]) .not.toMatchObject([{a: 1}]); expect([{a: 1}, {b: 2, c: 3}]) .toEqual(expect.arrayContaining([{a: 1}])); });
Matching it all
In some cases, not even the actual value of a property is interesting, just that it is there or that it has a certain type. expect.anything and expect.any are the methods of choice in these cases.
test('anything', () => { expect(5).toEqual(expect.anything()); expect(5).toEqual(expect.any(Number)); });
Errors
To test the logical flow when errors are thrown, Jest provides the toThrowError method to check that a function throws an error. Optionally, you can pass in an argument to assert the kind of error thrown. The following snippet shows the usage of the method. In order to avoid that the error is just thrown and not handled by Jest, you need to wrap the error-throwing function inside another function.
function divideBy10(a) { if (typeof a !== 'number') { throw new Error('NaN: ' + a); } return a / 10; } test('Errors', () => { expect(() => divideBy10('a')).toThrowError(); expect(() => divideBy10(100)).not.toThrowError(); expect(() => divideBy10('a')).toThrowError(/NaN/); expect(() => divideBy10('a')).toThrowError('NaN'); expect(() => divideBy10('a')) .toThrowError(new Error('NaN: a')); });
Asynchronous Assertions
Asynchronous operations using promises are very common in javascript applications. Jest supports testing for the outcomes of promises in several ways.
The simplest way is to evaluate the promise and put the assertion into the then()-clause. The important thing to look out for in this case is to make Jest aware that it has to wait for the promise to resolve. Otherwise the test will finish before the promise has been resolved and failed assertions will not fail the test. You can amend this by simply returning the promise from the test method:
test('Resolves - Evaluate promise by hand', () => { return Promise.resolve(true) .then(result => expect(result).toBe(true)); });
An equivalent to this approach, which might be a bit easier to read, is using the resolves method which unwraps the value of the promise:
test('Resolves - Unwrap promise result', () => { return expect(Promise.resolve(true)).resolves.toBe(true); });
If you prefer using async/await, this is possible as well, also in combination with the resolves method. In this case, the test methods do not need to return anything:
test('Resolves - Await result, then assert', async() => { const result = await Promise.resolve(true); expect(result).toBe(true); }); test('Resolves - Await result of assertion', async() => { await expect(Promise.resolve(true)).resolves.toBe(true); });
Tests for the rejection of a promise work analogously. Instead of resolves, you can use the rejects method. When you unwrap the promise by hand, you should ensure that the assertion was actually evaluated. Otherwise, if the promise resolves instead of rejects, the test will be green although no error was thrown. The method call expect.assertions(1) checks that 1 assertion method is triggered during the test run.
test('Rejects - Evaluate promise by hand', () => { expect.assertions(1); return Promise.reject('Error') .catch(error => expect(error).toBe('Error')); }); test('Rejects - Unwrap promise result', () => { return expect(Promise.reject('Error')).rejects.toBe('Error'); }); test('Rejects - Catch error, then assert', async() => { expect.assertions(1); try { await Promise.reject('Error'); } catch (error) { expect(error).toBe('Error'); } }); test('Rejects - Await result of assertion', async() => { await expect(Promise.reject('Error')).rejects.toBe('Error'); });
In the case of reject, I clearly prefer the rejects method over the manual unwrapping, since it makes the test a lot easier to read.
Bonus
If none of the assertions provided by Jest fulfills the use case, it is even possible to really simply write your own assertion using expect.extend. Below is a simple example:
describe('I need more matchers!', () => { expect.extend({ toBeGreaterZero(received) { const message = 'expected ' + received + (this.isNot ? ' not' : '') + ' to be greater zero'; return { message: () => message, pass: received > 0 }; } }); test('Is my number > 0?', () => { expect(1).toBeGreaterZero(); expect(-1).not.toBeGreaterZero(); }); });
The custom assertion is a function which always has the value passed to expect as the first argument, and can optionally take additional arguments. The returned object not only contains the information whether the assertion passed, but also an error message. Jest provides some utilities to really craft error messages and make them understandable and useful. In the example, I use the this.isNot property to check if not was used in the method chain and negate the message in this case.
Want to get started with Jest? The Jest homepage and docs is a great resource: https://jestjs.io