DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Testing with Jest: from zero to hero

I’ve been a regular user of Jest for quite some time. Originally, I used it like any other test runner but in some cases, I used it simply because it’s the default testing framework in create-react-app.

For a long time, I was not using Jest to its full potential. Now, I want to show you why I think it’s the best testing framework. Ever.

Snapshots

What are snapshots and why they are so handy?

The first time I saw this functionality I thought it was something limited to enzyme and react unit testing. But it’s not! You can use snapshots for any serializable object.

Let’s take a look.

Imagine you want to test if a function returns a non-trivial value like an object with some nested data structures. I have found myself writing code like this many times:

const data = someFunctionYouAreTesting()
assert.deepEqual(data, {
  user: {
    firstName: 'Ada',
    lastName: 'Lovelace',
    email: 'ada.lovelace@example.com'
  }
  // etc.
})

But, if some nested property is not exactly what you were expecting… You just get an error and you’ll need to find the differences visually!

assert.js:83
  throw new AssertionError(obj);
  ^

AssertionError [ERR_ASSERTION]: { user:
   { firstName: 'Ada',
     lastName: 'Lovelace!',
     email: 'ada.lovelace@example.com' } } deepEqual { user:
   { firstName: 'Ada',
     lastName: 'Lovelace',
     email: 'ada.lovelace@example.com' } }

If the function you are testing returns something random (for example, when you generate a random API key) you cannot use this mechanism anymore. In that case, you have to manually check field by field:

const data = someFunctionYouAreTesting()
assert.ok(data.user)
assert.equal(data.user.firstName, 'Ada')
assert.equal(data.user.lastName, 'Lovelace')
assert.equal(data.user.email, 'ada.lovelace@example.com')
// and it goes on...

This is better from a testing perspective, but it’s a lot more work.

If you find yourself doing these things, you’ll love snapshots!

You will write something like this:

const data = someFunctionYouAreTesting()
expect(data).toMatchSnapshot()

…and the first time the test runs, Jest will store the data structure in a snapshot file that you can manually open and validate. Any time you run the test again Jest will load the snapshot and compare it with the received data structure from the test. If there are any differences, Jest will print a colored diff to the output. Awesome!

Now, what if we don’t want to compare the whole structure (because some fields can be dynamic or can change from test to test)? No problem.

const data = someFunctionYouAreTesting()
expect(data).toMatchSnapshot({
  createdAt: expect.any(Date),
  id: expect.any(Number),
})

These are called property matchers .

But there’s more. One problem I found with this way of validating data structures is that the snapshot file is separated from the test code. So sometimes you need to jump from one file to another to check that the snapshot contains what you are expecting. No problem! If the snapshot is small enough you can use inline snapshots. You just need to use:

const data = someFunctionYouAreTesting()
expect(data).toMatchInlineSnapshot()

And that’s it! Wait… but where’s the snapshot?

The snapshot is not there… yet. The first time you run the test, Jest will accept the data structure and instead of storing it in a snapshot file it will put it in your code.

Yeah, it will change your testing code, resulting in something like this:

const { someFunctionYouAreTesting } = require("../src/app");
test("hello world", () => {
  const data = someFunctionYouAreTesting();
  expect(data).toMatchInlineSnapshot(`
Object {
  "user": Object {
    "email": "ada.lovelace@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace",
  },
}
`);
});

This blows my mind..

..and I love it. A development tool seamlessly changing your code is a simple and elegant solution that would be super useful in other scenarios. Imagine having a react/angular/vue development mode where you can visually edit components in the browser and the code is updated for you to match those changes!

By the way, if the test is not small enough to use inline snapshots you can still get some help. If you use Visual Studio Code with this extension you can see the snapshot on hover (it’s very useful even though it has some limitations).

Interactive mode

In the beginning, I thought the interactive mode was just a fancy term for the typical watch feature that many CLI applications have. But then I learned a few things.

Jest integrates with Git and Mercurial. By default, the watch mode will run only the tests affected by the changes made since the last commit. This is cool and makes me write more atomic commits too. If you are wondering how the heck Jest knows what tests are affected by commit changes, you are not alone.

The first thing Jest does is load the tests and thus load the source code of your application parsing the requires() and imports to generate a graph of interdependencies.

But using Git or Mercurial is not the only thing you can do to limit the number of tests to run every time. When I make changes to the source code and I see many failed tests I focus on the simplest test that fails. You can do that by using test.only but there’s a better way (I especially don’t like test.only or test.skip because it’s easy to forget about it and leave it in your code).

The “interactive way” is more elegant and convenient. Without editing your test code you can limit the tests to run in different ways.

Let’s take a look.

The simplest one is by hitting t and entering the name of the test. If you have test ‘hello world’,… hit t, write hello world and hit enter.

Well, that works in most cases, if you have a test(‘hello world 2’,… it will run too because you entered a regular expression. To avoid this, I usually add a $ at the end of the pattern.

In projects where there are many integration tests that hit the database, I found that running the tests was still slow. Why?

The thing is that filtering by test name does not prevent all of the before() and after() callbacks to be run in all of the other tests. And usually, in integration tests, those callbacks are where you put heavy stuff like opening and closing connections to the database.

So, in order to prevent that I usually also filter by file name. Just hit p (for path) and enter the file name that contains the test. You’ll find that the test runs a lot faster now (to return just hit t and clean the filter by hitting enter, do the same with the filter for file names with p and enter).

Another super handy feature is upgrading. When you see the diff and you see that the new snapshot is fine and the old one is outdated, just hit u (for upgrade) and the snapshot will be overwritten!

Two more useful options are a to run all the tests and f to run the failed tests again.

Batteries included

Another thing that I like is that Jest is a batteries included framework. Meaning you usually don’t have to add plugins or libraries to add common functionality to it. It just ships with it! Some examples:

  • Add coverage when invoking Jest and you get coverage reports from your tests with the ability to choose between a few built-in reporters or custom ones. You can even set a coverage threshold so your tests (and your CI) fails if that threshold is not met. Perfect for keeping up a good test coverage in your code.
  • Add notify and you’ll get desktop notifications when the test runner finishes. If you have thousands of tests, they can take a while to finish. Just by adding this flag you’ll optimize your time.
  • You don’t need to add an assertion library to your project in order to start writing powerful and useful assertions. You have an extensive expect functionality already built-in, ready to be used with interesting functionality such as the colored diffing that we also saw in the snapshot functionality.
  • You don’t need a library to mock functions or services. You have plenty of utilities to mock functions and modules and check how they were invoked.

Debugging with VSCode

Debugging Jest tests with VSCode is pretty straightforward.

Just go to the debugger tab and click the gear icon with the little red dot. Click on it and create a Node.js launch file. Now replace the content with the recipe you’ll find below.

This recipe is based on a separate recipe, which includes two configurations: one for running all tests and one for running just the current test file. I’ve added an additional configuration that allows you to select (in the text editor) the name of a test and run just that one! I’ve also added the watch flag so you can edit your code or your test, save it, and the test will re-run very quickly. This is possible because Jest loads the test by itself, not the underlying system (the Node.js runtime).

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Jest All",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--runInBand"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "windows": {
        "program": "${workspaceFolder}/node_modules/jest/bin/jest",
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Jest Current File",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["${relativeFile}"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "windows": {
        "program": "${workspaceFolder}/node_modules/jest/bin/jest",
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Jest Selected Test Name",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["${relativeFile}", "-t=${selectedText}$", "--watch"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "windows": {
        "program": "${workspaceFolder}/node_modules/jest/bin/jest",
      }
    }
  ]

Conclusions

Jest is not just a test runner, it is a complete testing framework that has brought testing to another level. It is not only super powerful but easy to use. If you are not using it yet, give it a try, you will not look back.


Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


The post Testing with Jest: from zero to hero appeared first on LogRocket Blog.

Oldest comments (0)