Jest asserts beyond equals - tips for improving assertions
The content here is under the Attribution 4.0 International (CC BY 4.0) license
Testing with jest is an activity that developers do to keep the application maintainable and time proof. Therefore, learning a testing framework can be a consuming task, often it has many features to explore. The assertion API (Application Programming Interface) usually is one of the most important, as this is the one that the developer uses the most during the TDD (Test Driven Development) [1] flow.
The gist of the assertion API [2] is to compare values, as such the equals match is the most used (in my experience). On the other hand being one of the most used can also point to a lack of knowledge in the different assertions that the testing framework offers. Sometimes this lack of understanding can lead to the common errors as the environment in which jest executes or the async behavior [3].
This post aims to cover different assertions, to avoid using always
toEqual
and make the test case more expressive. For each example, I try
to first depict how it would be with toEqual
, then I show another way
using a different assertion. Besides that, I also wrote about timers and how
to deal with that in jest, in this blog post,
it uses reactjs as a medium to depict a non deterministic use of time.
Note: there is a mind map for this content as a guide, you can access it any time as a way to visualize the content.
Assertions
This section focuses on the assertions that we can use and alternatives to
“assertion smells”. To make this point, the post follows an approach
comparing the assert.toEqual
approach against a more expressive assertion
for the scenario.
Any
Any
is a generalization to use when the value of the result is not needed, rather
the type is.
const isNumber = number => number
expect(typeof isNumber(2)).toEqual('number')
An alternative to this approach would be to use the any:
const isNumber = number => number
expect(isNumber(2)).toEqual(expect.any(Number))
Array Containing
Thinking about assert.equal
, an approach to assert an entry of arrays, would
be to go through them and assert each of them, for example:
const expectedFruits = ['banana', 'mango', 'watermelon']
expect(expectedFruits[0]).toEqual('banana')
expect(expectedFruits[1]).toEqual('mango')
expect(expectedFruits[0]).toEqual('watermelon')
Therefore another approach to assert such structure is using arrayContaining
:
const expectedFruits = ['banana', 'mango', 'watermelon']
const actualFruits = () => ['banana', 'mango', 'watermelon']
expect(expectedFruits).toEqual(expect.arrayContaining(actualFruits))
to Be
toBe
is a stricter way of asserting values.
to Have Length
For checking the size of an array is possible using the length
property. There
are different ways to achieve that, for example, with assert equals, would be
something:
const myList = [1, 2, 3]
expect(myList.length).toEqual(3) // <---
Therefore, jest offers a matcher specifically for that, instead of asserting
the length
property. The same snippet using toHaveLength
would become:
const myList = [1, 2, 3]
expect(myList).toHaveLength(3) // <---
to Be Greater Than
Asserting values greater than other can be achieved with raw assert.equals
,
such as:
const expected = 10
const actual = 3
expect(expected > actual).toEqual(true)
The disadvantage here is that when reading the assertion it takes a bit more to interpret the code in our head. For that, jest offers an assertion that is more readable to follow (and also gives a more friendly message when failing).
const expected = 10
const actual = 3
expect(actual).toBeGreaterThan(expected)
Modifiers
not
The not modifier is handy when it comes to assert the negation of a given
sentence. For context, a indication that .not
is needed would
be asserting false in some result, for example:
const isOff = false
expect(!isOff).toBe(true) // <--- this sometimes is tricky to spot
Another way to achieve the same result but being explicitly would be something as follows:
const isOff = false
expect(isOff).not.toBe(true)
The .not
operator can be used across different assertions within jest.
Async
Jest provides an API for a more readable test code and to assert async functions. It is easy to fall under the trap of using assert equals after a promise has been fulfilled. Besides that, Martin Fowler points out that asynchronous behavior is part of the non determinism club, which can lead to tests failing without any change in the code [4], those are the opposite of deterministic tests1.
Resolves
Testing async code comes with challenges and the approach to test also changes. One way to test is to use the variable that comes from the it callback, something like:
it('my async test', done => {
callAsyncFunc().
then((value) => {
expect(value).toBe(true)
done()
})
})
The code above depicts how to assert a value once the promise resolves. Jest
provides a more readable way of doing things with resolves
:
it('my async test', async () => { // <--- 1
await expect(callAsyncFunc()).resolves.toEqual(true) // <--- 2
})
The same applies to a rejected promise, in this case we would change the resolves
by rejects
.
it('my async test', async () => {
await expect(callAsyncFunc()).rejects.toEqual(false) // <--- 3
})
Callbacks
Callbacks are the heart of javascript and when testing them an async style is used as well, as the callback might/might not be called in a different time in the execution flow.
to Have Been Called
Asserting that a callback has been invoked can be achieved in different ways, for this purpose the first approach (and not recommend) is to use the async style as in the previous example:
it('callback has been invoked', done => {
callAsyncFunc(() => {
expect(true).toEqual(true) <--- assumes it has been called
})
})
A more readable assertion would be using toHaveBeenCalled
, as it is human
readable and might take less time to understand what the test case is
asserting
it('callback has been invoked', done => {
const result = jest.fn() // 1
callAsyncFunc(result)
expect(result).toHaveBeenCalled() // 2
})
- jest uses this spy to assert calls against it
- assert that the function has been called, regardless of the number of calls
to Have Been Called Times
Asserting that a function has been called is the most basic assertion in this
respect. There are variants that are more strict than that. For example, it
is possible to assert that a given function have been called X times, as
opposed to toHaveBeenCalled
that does not match exactly the number of calls.
it('callback has been invoked', done => {
const result = jest.fn()
callAsyncFunc(result)
expect(result).toHaveBeenCalledTimes(4)
})
The code above assert that the given spy is called 4 times, any number different than that will fail the test case.
Wrapping up
I hope that those examples will give you the chance to explore jest as a way to improve your assertions and your test readability.
As always: happy testing!
Edit Aug 24, 2021 - TDC INNOVATION
The following slides were used as a based to my talk and this blog post was a guide to build it. The slides offer a more visual approach to the same content.
Edit Nov 18, 2021 - Codurance
Slides used for Codurance talk
Edit Jan 08, 2022 - Codurance
Post available in Spanish - last accessed Jan 08, 2022.
References
- [1]K. Beck, TDD by example. Addison-Wesley Professional, 2000.
- [2]Jest, “Expect,” 2021 [Online]. Available at: https://jestjs.io/docs/expect. [Accessed: 26-Apr-2021]
- [3]J. Njenga, “ITS JEST — COMMON PROBLEM FACED USING JEST,” 2020 [Online]. Available at: https://medium.com/@joenjenga/its-jest-common-problem-faced-using-jest-9905e96db8a. [Accessed: 05-Sep-2021]
- [4]M. Fowler, “Eradicating Non-Determinism in Tests,” 2011 [Online]. Available at: https://martinfowler.com/articles/nonDeterminism.html. [Accessed: 21-Jun-2021]
Related subjects
Footnotes
-
Kent Beck and Kelly Sutton talk about determinism (isolated tests). Isolated tests means, test to run by themselves. ↩
Table of contents
Got a question?
If you have question or feedback, don't think twice and click here to leave a comment. Just want to support me? Buy me a coffee!