Testing JavaScript Applications with Jest
Introduction
Writing tests for JavaScript applications can be quite fun and easy if you find a tool you like. In this post I will talk about my favorite way to test React.js applications, namely the Jest Framework .
Before you even start writing tests
Consider using static type-checking with a tool like Flow or Typescript in your project. This catches a lot of compile-time bugs. Also setting up a linter like ESLint is worth a lot!
Testing Overview
Some general pointers before we get started:
- Make tests visible by putting them close to the file you’re testing (at least unit tests). Don’t let them wither and die in some dark corner of your app directory.
- Test like your software is used, don’t test implementation details. Think about what steps the user wants to accomplish and mimic him as close as possible.
The Jest Framework
Jest was created by Facebook and runs all their tests, including React apps. If it’s good enough for them it should be good enough for us.
It runs tests in parallel and doesn’t touch the DOM by running in Nodejs (utilizing JSDOM
). Those features make it really fast! It runs failed tests first so you get quick feedback, comes with a nice watch mode, an ability to run snapshot tests and a powerful mocking library.
Setting it up
If you use create-react-app
Jest comes pre-installed along with a npm script to run it (npm run test
). Generating a code coverage overview is as simple as running jest --coverage
. If you’re not using cra
it can be a bit tricky to get set up for the first time. As I’m not an expert on this I will leave this to others to write about, a google search should find some good posts.
Writing unit tests
Jest finds your tests automatically, as long as they are in a folder called __tests__
or the filenames themselves contain .test
or .spec
.
Some example file names: register.test.js
or somecomponent.spec.js
.
watch mode
Jest’s watch mode is super nice and lets you filter your tests in ways so only the tests you want will be running. This is the interface you see when it runs in watch mode:
› Press o to only run tests related to changed files.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
Mocking with Jest
Mocking out dependencies (like API calls) is very powerful but contains quite some magic in Jest . This excellent post by Rick Hanlon II is a must-read.
Mocking Function with jest.fn()
The simplest mocking function can be created with this function. It creates a function returning undefined
but collects data about how it was called. Let’s say you want to test a React login form component that takes a handleSubmit
prop function that usually calls an API to log your user in after the user filled out the login form and pressed the submit button. We just want to test if this component works, not the API, so we mock it out:
const myMock = jest.fn();
render(<Login onSubmit={myMock} />);
expect(handleSubmit).toHaveBeenCalledTimes(1);
expect(handleSubmit).toHaveBeenCalledWith(/* the correct form input */);
jest.fn()
can also take a callback if you want your mock to return something
const myMock = jest.fn(() => true);
myMock(); // returns true
Mocking modules with jest.mock()
Let’s say you use aws-amplify
’s Auth
module to log your users in. You can mock the whole Auth
module by running
jest.mock('aws-amplify')
somewhere in your test (jest.mock
calls get hoisted to the top so it doesn’t interfere with imports). Now jest looks for a aws-amplify.js
file in a __mocks__
folder at the same level as your node_modules
. In it we have this:
// in __mocks__/aws-amplify.js
export const Auth = {
currentSession: jest.fn(() => Promise.resolve()),
signIn: jest.fn(() => Promise.resolve()),
signOut: jest.fn(() => Promise.resolve()),
};
Now all calls to one of these three functions of the aws-amplify.Auth
module just return a resolved Promise
and you can use the same assertion functions like
expect(Amplify.Auth.signIn).toHaveBeenCalled();
Spying with jest.spyOn()
If you want to have these handy assertions like .toHaveBeenCalled()
or other
but would like to leave the original implementation in place, this function is for you. Use it like this:
// Math.add exists
const mySpy = jest.spyOn("Math", "add");
Math.add(1, 2); // 3
expect(mySpy).toHaveBeenCalledWith(1, 2);
So Math.add() gets runs normally when it’s spied on! This can trip some people up coming from other testing libraries.
Snapshot Testing
Snapshots are a serialization of the dom to check if your UI has not changed unexpectedly. They are quite easily generated like this (example from the Jest docs):
import React from "react";
import Link from "../Link.react";
import renderer from "react-test-renderer";
it("renders correctly", () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
While this sounds at first like the solution to everything, they are not without problems. To get a better understand read Effective Snapshot Testing by Kent C. Dodds.
Additional tools
I’m a big fan of Kent C. Dodds’ react-testing-library which aims to be a replacement to Airbnb’s Enzyme . I will probably write a separate post specifically about testing with this library in the future.
If there’s something missing or you would like me to write about contact me via email or twitter!