Setting up Jest unit tests in a React + Typescript project
Here’s what I do when I want to set up a Jest on a React project.
First, we need to install some dependencies:
npm install --save-dev jest babel-jest ts-jest chai chai-jest identity-obj-proxy
What are all of these?
jest
is a unit test runner from Facebook. You probably already know this, because you’re reading this.ts-jest
is atransform
for jest which compiles typescript files. If you’re using babel to compile your typescript files, you can skip this.babel-jest
is likets-jest
, but uses babel to transform files - handy if you have a project with some mixed typescript and javascript.chai
is an assertion library. Jest already comes with anexpect
built in, but if you’re coming from mocha you probably already use chai, and it’s somewhat more expressive and has a lot of plugins available.chai-jest
is a plugin for chai which has supports jest mocks. It basically re-implements a bunch of the jest-specific asserts on the built-inexpect
object. See the documentation for a list of assertions available.identity-obj-proxy
is a handy library for cases where we import files like CSS modules. If youimport styles from 'styles.css'
, then we can configure jest to import ‘identity-obj-proxy’ for *.css, and then when you dostyles.container
, it will resolve to “container” instead of throwing an exception.
Then we’re going to create some files to set up jest. First, jest.config.js:
// jest.config.js
module.exports = {
globals: {
"ts-jest": {
// Tell ts-jest about our typescript config.
// You can specify a path to your tsconfig.json file,
// but since we're compiling specifically for node here,
// this works too.
tsConfig: {
target: "es2019",
},
},
},
// Transforms tell jest how to process our non-javascript files.
// Here we're using babel for .js and .jsx files, and ts-jest for
// .ts and .tsx files. You *can* just use babel-jest for both, if
// you already have babel set up to compile typescript files.
transform: {
"^.+\\.jsx?$": "babel-jest",
"^.+\\.tsx?$": "ts-jest",
// If you're using babel for both:
// "^.+\\.[jt]sx?$": "babel-jest",
},
// In webpack projects, we often allow importing things like css files or jpg
// files, and let a webpack loader plugin take care of loading these resources.
// In a unit test, though, we're running in node.js which doesn't know how
// to import these, so this tells jest what to do for these.
moduleNameMapper: {
// Resolve .css and similar files to identity-obj-proxy instead.
".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
// Resolve .jpg and similar files to __mocks__/file-mock.js
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `<rootDir>/__mocks__/file-mock.js`,
},
// Tells Jest what folders to ignore for tests
testPathIgnorePatterns: [`node_modules`, `\\.cache`],
testURL: `http://localhost`,
};
We need to create the “mocks/file-mock.js” referenced above in “moduleNameMapper”, too. This file will be imported in place of “.jpg” or “.mp3” or similar files:
// __mocks__/file-mock.js
module.exports = "test-file-stub";
Resolving Custom Paths
In your tsconfig.json file, you can set up a “paths” section:
"paths": {
"components/*": ["src/components/*"],
}
This lets you import Button from "components/Button"
, and helps you avoid long chains of “../../..” in your code. But, we need to make jest know about these imports. The easiest way to do this, if you’re already using webpack, is via the jest-webpack-resolver. In your webpack configuration, you probably already have something like:
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
...
resolve: {
modules: ['node_modules'],
plugins: [new TsconfigPathsPlugin({ extensions })],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
...
}
So we’re going to take advantage of the fact that webpack knows how to resolve things already:
npm install --save-dev jest-webpack-resolver
And then somewhere in your jest.config.js add:
module.exports = {
...
resolver: 'jest-webpack-resolver',
...
}
If you are not using webpack, then check out tsconfig-paths-jest as a possible alternative.
Running tests
At this point you should be able to:
npx jest
And it should run your tests!
Example Test File
Here’s a quick example of a test file - if you have a Button.tsx
, you might put this in Button.test.tsx
in the same folder. Note that this uses the @testing-library/react
and @testing-library/user-event
libraries:
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import chai from "chai";
import chaiDom from "chai-dom";
chai.use(chaiDom);
const { expect } = chai;
describe("Test Suite", () => {
beforeEach(() => {
// TODO: Uncomment this if you're using `jest.spyOn()` to restore mocks between tests
// jest.restoreAllMocks();
});
it("click on a button", () => {
const { getByText } = render(<button>Hello World</button>);
// `getByText` comes from `testing-library/react` and will find an element,
// or error if the element doesn't exist. See the queries documentation
// for info about other query types:
// https://testing-library.com/docs/dom-testing-library/api-queries
const button = getByText("Hello World");
// `userEvent` is a library for interacting with elements. This will
// automatically call `React.act()` for you - https://reactjs.org/docs/test-utils.html#act.
userEvent.click(button);
});
});
Note that all tests run in node.js, where this is no browser. By default, Jest will initialize a jsdom
environment for you, which gives you a window
and a document
and will let you render nodes to a virtual screen. But, if you’re writing a test for a module that doesn’t need to interact with the DOM, you can speed up a test by using the “node” jest environment which will skip all of that:
/**
* @jest-environment node
*/
import chai from "chai";
import chaiJest from "chai-jest";
chai.use(chaiJest);
const { expect } = chai;
describe("Test Suite", () => {
it("should do the thing", () => {
// TODO
});
});