Should you use jest as a testing library?

Learn why it breaks the instanceof operator

ยท

4 min read

jest, the popular testing framework created by Facebook with over 50 million downloads per month, causes many problems for backend developers.

In this article, I try to recap the jest's cross and delight and how to deal with it and what is causing it.

How does jest work?

jest is a JavaScript Testing Framework with many features, including isolated written on its website. The isolation feature's target performance:

Tests are parallelized by running them in their own processes to maximize performance.

The jest's isolation comes from its architecture that uses the node:vm core module under the hood.

The vm module lets jest run every test file into a sandbox with its own temporary context. The context includes all the global classes such Array, Error, Date and many others - the describe and the it test function for example. (Here the jest source code that does the trick)

Since jest overwrites some of those components to provide you fancy features like mocks, fake clocks and a fast test execution, it must set all the vm's global data to set up the context where the test's source code will be executed.

Unfortunately, when the Node.js core modules create a new instance of a global class, they will not use the vm's context, but they fall back to the native implementation.

This means that the instanceof operator will not work as expected, and it will generate false negatives!
You can get a quick example of this problem in the following test.js snippet file:

const { parseArgs } = require('util')

test('Array is not an Array', async () => {
  const { values } = parseArgs({
    args: ['--bar', 'a', '--bar', 'b'],
    options: {
      bar: {
        type: 'string',
        multiple: true
      }
    }
  })
  expect(values.bar).toEqual(['a', 'b'])
  expect(values.bar).toBeInstanceOf(Array) // โŒ it will fail
})

By running the above test with the command jest test.js (with the default jest configuration) it will fail with the stange error:

  โ— Array is not an Array

    expect(received).toBeInstanceOf(expected)

    Expected constructor: Array
    Received constructor: Array

      27 |   })
      28 |   expect(values.bar).toEqual(['a', 'b'])
    > 29 |   expect(values.bar).toBeInstanceOf(Array)

This may seem a minor problem, you need just to avoid using the instanceof operator, but it is not. The bigger problem is that the instanceof operator could be used by your dependencies tree to perform some checks, and those conditions will fail.

For example, Fastify removed the instanceof operator from its codebase because it was causing problems for those developers that rely on jest as a testing framework.

How to fix it?

It depends ๐Ÿ˜„

You can't out of the box. There is an open issue on the Node.js repository to let the node:vm module to use the vm's context, but it is still open. It seems that the Node.js core team is interested in fixing this problem by implementing the new ShadowRealm spec, and I think we will make some progress during 2023.

If you can't wait, there is a very quick solution by using the jest-environment-node-single-context custom test enviroment.
If you install this module and you run the previous code with the command:

jest --testEnvironment jest-environment-node-single-context test.js

Now, all will work as expected:

 PASS  ./test.js
  โœ“ Array is not an Array (5 ms)

โš ๏ธ Note that now the test is working because the jest isolation feature is totally disabled by running all the tests in the same context.

Another working solution is to use a new jest runner: jest-light-runner. It spin up a Node.js worker thread for each test file giving you the same isolation feature of jest but without the vm's context. Note that not all the jest features are supported by this runner, but all the most used features are working!
So, after installing it, you can verify that the tests are green by running the command:

jest --runner jest-light-runner test.js

Summary

jest is a great testing framework, and it works well for frontend applications, but you could face some issues if your dependencies rely on instanceof.

We discussed about how to fix this problem in different ways:

  • Adopt the jest-environment-node-single-context custom test environment and its limitations
  • Use the jest-light-runner runner to get the same isolation feature of jest but with a subset of its features
  • Open an issue to the repository of the module that you are using and ask to remove the instanceof operator, that is still a good practice
  • Choose another test framework. I personally prefer node-tap

The jest architecture makes sense for frontend applications that run in the browser and has a different global context, but it does not suit the case for Node.js applications.
I don't like the concept of using a different global context in my test and production environment.

Now jump into this article source code to try the code snippets I wrote to verify my findings.

If you enjoyed this article, comment, share and follow me on Twitter!

ย