The first thing we need to start implementing tests in any project is to install the right tool. In my case, I’ve chosen jest, and today we’re going to see how to install and configure it.
Why Jest?
Jest is a library that lets us write and run tests. It’s developed by Facebook and used by platforms like airbnb, twitter, spotify, resuelve, etc.
Some of the features that I think make Jest the best option today are:
- Almost zero configuration to get started — it depends on what your project uses
- It’s extremely fast, using workers to parallelize execution
- It supports snapshot testing
- Error/feedback messages are very clear
- It detects and uses your babel configuration
- You can get coverage reports without installing anything extra
- It’s extensible — you can create your own custom matchers or even run tests from other languages
- You can run frontend and backend tests, running in parallel if you have a monorepo
Installing Jest
As I was saying, installing Jest requires almost zero configuration. We just need to add jest to our dependencies with:
npm install --save-dev jest
Then we can create a script to run it by adding this to package.json:
{
"scripts": {
"tests": "jest"
}
}
Now, if you run npm run tests, it’ll run Jest but throw an error saying it didn’t find any test files. To see it working, we need to add one.
By default, it looks for files inside a __tests__ folder whose names end with .test.js or .spec.js. It also ignores everything inside node_modules by default.
The “hello world” of testing is simply creating a file (wherever you like) inside the project at __tests__/index.test.js and adding the following content:
// __tests__/index.test.js
describe('initial', () => {
test('first tests', () => {
expect(true).toBe(true)
})
})
This simply tests that true equals true. If you run npm test again, you’ll see it says one test passed.
With that, we’d have everything we need for simple projects. But what happens if we use things like importing CSS/SCSS in JS, dynamic imports, ES modules, etc.? Let’s see how to configure some specific things.
Configuring ES Modules in Jest
Many of the modern projects you’ll work on will be using ES Modules. But normally when we configure babel, we skip module transpilation because webpack supports this syntax and uses it for tree shaking when building the project.
The configuration should look something like:
// .babelrc
{
"presets": [["env", { modules: false }]]
}
Now we need to tell it that when we run tests, it should transpile modules, but in production it shouldn’t. We have two options.
First option: use the configuration as a .js file:
// babelrc.js
const isTest = String(process.env.NODE_ENV) === 'tests'
module.exports = {
presets: [['env', { modules: isTest ? 'commonjs' : false }]]
}
And inside package.json, reference the configuration using:
{
"babel": {
"presets": "./babelrc.json"
}
}
Second option: use babel environments:
{
"env": {
"test": {
"presets": [["env", { modules: 'commonjs' }]]
},
"production": {
"presets": [["env", { modules: false }]]
}
}
}
This way, when we run the tests, Jest will be able to understand the imports since babel will convert them to commonjs. But in production, webpack will be able to do tree shaking and everything will work as it should.
Environments in Jest
By default, Jest loads js-dom, which basically provides an object that simulates the browser’s window. But we don’t always want to load this giant object — we might want to use it for testing a Node project.
To configure the environment we want, we can add this to package.json:
{
"jest": {
"testEnvironment": "node"
}
}
or:
{
"scripts": {
"tests": "jest --env=node"
}
}
This way we won’t load js-dom, and we’re all set to test code from a Node project.
On npm you can find many other environment packages like jest-environment-electron, jest-environment-webdriver, or jest-environment-puppeteer.
If you use a monorepo in your project and want to run backend and frontend tests with a single execution, Jest supports multi-project configurations where you can run different test suites in parallel, each with its own configuration:
// package.json
"jest": {
"projects": [
{
"displayName": "Frontend",
"testMatch": ["<rootDir>/frontend/*.js"],
"testEnvironment": "jsdom"
},
{
"displayName": "Backend",
"testMatch": ["<rootDir>/backend/*.js"],
"testEnvironment": "node"
},
{
"displayName": "lint",
"runner": "jest-runner-eslint",
"testMatch": ["<rootDir>/**/*.js"]
}
]
}
As you can see, we could also configure custom runners — in this case, running the linter with Jest.
Importing CSS in JS
When we import CSS files in our JS files — since we use a loader that supports it with webpack — it will cause Jest to fail because this isn’t valid JavaScript syntax. To make Jest handle it, we need to detect all CSS imports and handle them to avoid the failure. To do this, add the following to package.json:
{
"jest": {
"moduleNameMapper": {
"//.css$": "./test/cssMock.js"
}
}
}
What we’re doing is telling Jest that when it finds an import matching the .css regex, it should import a replacement file instead, which can be something like:
// cssMock.js
module.exports = {}
This also works if you’re loading .graphql, .svg, or image files inside JS.
Using CSS Modules with Jest
When we use CSS modules and use the approach above, the tests will work. But ideally we’d like to see what style is being applied to a node. For that, we can use identity-obj-proxy.
First, install it with npm i -D identity-obj-proxy and add it to the Jest configuration:
// package.json
{
"jest": {
"moduleNameMapper": {
"//.module.css$": "identity-obj-proxy"
}
}
}
Now when we use CSS modules like:
import styles from 'style.css'
const Title = ({ text }) => {
return <h1 className={style.title}>{text}</h1>
}
export default Title
And use the component in a test, we’ll get something like:
<h1 class="title">Hola</h1>
Using Dynamic Imports with Jest
If we import modules dynamically in our projects, we’ll run into problems. Since we previously configured imports to be converted to commonjs, and commonjs doesn’t support dynamic imports, we need to configure support for them. For this, we’ll use babel-plugin-dynamic-import-node.
Inside the babel configuration:
{
"env": {
"test": {
"presets": [["env", { modules: 'commonjs' }]],
"plugins" ["dynamic-import-node"]
},
"production": {
"presets": [["env", { modules: false }]]
}
}
}
Again, this is something we only want to do when our environment is set to testing.
Using LocalStorage
For now, js-dom doesn’t support local storage — we need to add it ourselves. We can use jest-localstorage-mock and call it in the configuration:
// package.json
{
"jest": {
"setupFiles": ["jest-localstorage-mock"]
}
}
setupFileslets you specify a series of modules that should run before the tests.
Or we could write our own version of local storage (there’s a good issue about this topic). For this, we can use setupTestFrameworkScriptFile, which lets us run a file before the tests execute:
// package.json
{
"jest": {
"setupTestFrameworkScriptFile": "./testSetup.js"
}
}
The file could contain an implementation like:
// testSetup.js
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
setItem(key, value) {
this.store[key] = value.toString();
}
removeItem(key) {
delete this.store[key];
}
}
window.localStorage = new LocalStorageMock();
Watch mode
If we’re writing tests, we wouldn’t want to make changes and run them manually every single time. With watch mode, it detects changes and re-runs the tests. To use it, just add --watch:
{
"scripts": {
"tests:watch": "jest --watch"
}
}
Some interesting things about watch mode:
- By default, it only runs tests that were added since the last commit
- It runs the last failed test first
- You can watch a single file or test
Coverage
Another thing we’d like to see when adding tests is how much of our code the tests are covering. For this, we use coverage. With Jest, it’s super easy — just add --coverage when running Jest:
// package.json
{
"scripts": {
"tests:coverage": "jest --coverage"
}
}
Once you run it, it will also generate files so you can view a report from the browser. You can open it at coverage/lcov-report/index.html.
Sometimes we also want to ignore certain files from our coverage. For this, we can use collectCoverageFrom, which lets us specify the files we want to include in the coverage:
{
"jest": {
"collectCoverageFrom": [
"**/src/*.{js,jsx}",
]
}
}
This way, we’re telling it to only include files inside src with .js or .jsx extensions. By default, it already excludes our files inside __tests__ folders.
Final words
Many of the cases we covered really depend on what’s being used in the project. You don’t need to set up all these configurations — in most cases, just installing Jest is enough.
In the next posts, we’ll start looking at how to write tests.