DEV Community

Cover image for Setting up Jest + React-Testing-Library
aromanarguello
aromanarguello

Posted on • Updated on

Setting up Jest + React-Testing-Library

In the past few months, I've had the immense pleasure to work with what I can say is my favorite library to use in the React + Friends environment, react-testing-library. This library is a piece of art.

There hasn't been a library more intuitive, easy to set up and more beginner friendly. In some occasions, it has been even a great way to break in a developer into a new project. With this guide, my aim is to share what I have learned in the process of configuring tooling for different projects and sort of the best practices that I have personally adopted. We will walk through the steps to get your environment setup with create-react-app and setting up a React project from scratch (jest*, web-pack, babel, etc…).
More than a how-to test guide, this guide is a step by step walkthrough on how to set up your testing environment in a React application. For learning how to write a unit test there are plenty of guides by more knowledgeable people than myself going over that topic. However, I will say that for me, the best way to become better at testing is to actually write tests. One of the biggest struggles I see both new and old developers who haven't written test, is learning the distinction between what Jest's role is and what react-testing library's role. In my own experience, the way to learn that distinction is by repetition, getting stuck, doing research, reiterating.

Eager to hear feedback from the community!

Index:

  • Directory file structure & conventions I've picked up
  • Getting started with Jest + RTL and create-react-app
  • Getting started with Jest + RTL from scratch
  • Config to setup with Typescript
  • Examples
  • Curated resources to help you get started with RTL

File structure:

Article I wrote on How I structure my React Apps (not with hooks):
https://blog.usejournal.com/how-i-structure-my-react-apps-86e897054593

Best practices and convention has been for the longest time, even before react was around, to create a folder __ test __ and just put your test files inside that folder. I have kinda done things differently and this is absolutely just personal preference. As I kept working with it the following system it kinda just stuck and the team I work with and my self pretty much enjoy it (I think!).

The typical file structure in my projects:

- node_modules
- public
- src
  - components
    - MyComponent
      - MyComponent.jsx
      - MyComponent.styles.js
      - MyComponent.test.js      // here is what I do different
    - index.js            // source of truth for component export
  - utils
    - helpers.js
  - pages
  - App.jsx
  - App.test.jsx
  - App.styles.js
  - index.js
Enter fullscreen mode Exit fullscreen mode

As I point out in the comment above. This is my biggest personal deviation from the popular convention. It just seems to me that in the age of component driven development it makes more sense for me to create this sort of encapsulated environments for your components (most important thing is to be consistent and work with what makes you comfortable 😁). Adding one test folder for each component you have, which in a large code base, with a lot of components and component variations, it seems like something that just not DRY. Additionally, I do not find any personal benefit behind adding that folder. Besides when jest crawls your root directory and looks for files to run it's not looking for a folder in particular (well, depends on your jest's RegEx pattern).

Naming and casing conventions:

- PascalCase for component file name and folder name
- Generally, I want to indicate if my components are container or component.
  Containers will usually be class components that contain state and logic,
  whereas components will house the actual content, styling and receive props from the container. 
  Example:
  - `MyComponent.container.js`
  - `MyComponent.component.js`
  - `MyComponent.jsx`          // if no container
  - `MyComponent.styles.js`
- lowerCamelCase for Higher Order Component file and folder name
- lowercase for all other root directory folders. For example: `src`, `components`, `assets`
Enter fullscreen mode Exit fullscreen mode

Some conventions worth noting

Describe method:

describe('My component', () => {
  // group of test()
})
Enter fullscreen mode Exit fullscreen mode

Describe method is one of what jest calls Globals methods, which you don't have to import or require to use. The describe statement, in particular, is used to group similar test together.

Test method

test('some useful message', () => {
   // logic
}, timeout) // timeout is optional
Enter fullscreen mode Exit fullscreen mode

Test functions are the bread and butter though. This is the function that actually runs your tests. According to Jest's documentation, the first argument is the name of the test, the second argument is the callback where you add your testing logic (assertions, etc.), and the third argument, which is optional, is the time out.

The test function also has an alias that can be used interchangeably it(): it('test', () => {})


Getting started jest and RTL with CRA:

Full disclosure. I love using CRA it setups everything for you and reduces the amount of technical overhead you will get over time as dependency versions fall behind. With react-scripts, you pretty much just have to worry about that part.

npx create-react-app ProjectName

npx create-react-app ProjectName --typescript

right off the bat, the first thing I do is install dependencies needed:

npm install --save-dev @testing-library/jest-dom

npm install --save-dev @testing-library/react

In the package.json file I add the following script:

"test": "jest -c jest.config.js --watch"
Enter fullscreen mode Exit fullscreen mode

Quick note: first thing when I start a new react project is to add those dependencies + styled-components and my types if need be.

Testing library documentation defines jest-dom as a companion library for React Testing Library that provides custom DOM element matchers for Jest. In essence, it is the dependency that provides statements (or matchers*) such as toHaveStyles or toHaveAttribute.

Example:
expect(Component).toBeInTheDocument() <- matcher

Once your project is created, inside my src folder I add a file called setupTests.js.

- src
  - components
  - App.js
  - setupTests.js
Enter fullscreen mode Exit fullscreen mode

The setupFiles is executed before the test framework is installed in the environment. For our case, it is especially important, because it will allow us to run the correct imports before the tests are executed. This gives us the opportunity to add a couple of imports.

So in your setupTests.js file:

import '@testing-library/jest-dom/extend-expect'
Enter fullscreen mode Exit fullscreen mode

And that's it for that file :).

This is all you need to get up and running with jest and react-testing-library!


Getting started on jest and RTL with a React app from scratch:

This part will be a little longer since there are more tools to cover and configure. In a way, we will walk through my step by step process to build a react application from scratch. create-react-app does abstract a lot of the configuration complexity and it does it really well, now we have to configure our babel and for our case most importantly the jest configuration. Higher overview the jest config takes care of making sure that jest knows where to look for, what to look for and how to execute it.

A Great resource to setup your React App from scratch:
https://blog.bitsrc.io/setting-a-react-project-from-scratch-using-babel-and-webpack-5f26a525535d

Directory structure

- node_modules`
- public
  - index.html
- src
  - components
    - MyComponent
      - MyComponent.jsx
      - MyComponent.styles.js
      - MyComponent.test.js      // here is what I do different
    - index.js             // source of truth for component export
  - utils
  - pages
  - App.jsx
  - App.test.jsx
  - App.styles.js
  - store.js
  - index.js
- webpack.config.js
- jest.config.js
- .gitignore
- .eslintrc
- .prettierrc
Enter fullscreen mode Exit fullscreen mode

index.html:

<!DOCTYPE html>
  <html lang="en">    
    <head>        
      <meta charset="UTF-8" />        
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />        
      <meta http-equiv="X-UA-Compatible" content="ie=edge" />                                    
      <title>React JS + Webpack</title>
    </head>    
    <body>        
      <div id="root"></div>    
    </body>
  </html>
Enter fullscreen mode Exit fullscreen mode

App.js

import React from 'react';

const App = () => <h1>Hi World</h1>;

export default App;
Enter fullscreen mode Exit fullscreen mode

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

webpack.config.js:

const webpack = require("webpack");

// plugins
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "./main.js"
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ["file-loader"]
      },
      { test: /\.jsx$/, loader: "babel-loader", exclude: /node_modules/ },
      { test: /\.css$/, use: ["style-loader", "css-loader"] }
    ]
  },
  devServer: {
    contentBase: "./dist",
    hot: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      filename: "./index.html"
    }),
    new webpack.HotModuleReplacementPlugin()
  ]
};
Enter fullscreen mode Exit fullscreen mode

jest.config.js:

module.export = {
  roots: ['<rootDir>/src'],
  transform: {
    '\\.(js|jsx)?$': 'babel-jest',
  },
  testMatch: ['<rootDir>/src/**/>(*.)test.{js, jsx}'], // finds test
  moduleFileExtensions: ['js', 'jsx', 'json', 'node'],
  testPathIgnorePatterns: ['/node_modules/', '/public/'],
  setupFilesAfterEnv: [
    '@testing-library/jest-dom/extend-expect'', 
    '@testing-library/react/cleanup-after-each'
  ] // setupFiles before the tests are ran
};
Enter fullscreen mode Exit fullscreen mode

MyComponent.js:

import React from 'react'
import styled from 'styled-components'

const MyComponent = props => {

  return (
    <h1>`Hi ${props.firstName + ' ' + props.lastName}!`</h1>
  )
}
export default MyComponent

Enter fullscreen mode Exit fullscreen mode

MyComponent.test.js:

import React from 'react'
import { render, cleanup } from '@testing-library/react'
import MyComponent from './MyComponent'

afterEach(cleanup)

describe('This will test MyComponent', () => {
  test('renders message', () => {
     const { getByText }= render(<Mycomponent 
                                 firstName="Alejandro"
                                 lastName="Roman"
                              />)

     // as suggested by Giorgio Polvara a more idiomatic way:
     expect(getByText('Hi Alejandro Roman')).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

input example:

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Input from './Input'


test('accepts string', () => {
  // I try to avoid using data-testid as that is not something a user would
  // use to interact with an element. There are a lot of great query and get 
  // methods
  const { getByPlaceholderText } = render(<Input placeholder="Enter
                                           Text" />);
  const inputNode = getByPlaceholderText('Search for a problem or application name');

  expect(inputNode.value).toMatch('') //tests input value is empty

  // if you need to perform an event such as inputing text or clicking
  // you can use fireEvent
  fireEvent.change(inputNode, { target: { value: 'Some text' } }));

  expect(inputNode.value).toMatch('Some text'); // test value 
                                                // is entered
});
Enter fullscreen mode Exit fullscreen mode

Typescript config

tsconfig.json:

{
  "include": [
    "./src/*"
  ],
  "compilerOptions": {
    "lib": [
      "dom",
      "es2015"
    ],
  "jsx": "preserve",
  "target": "es5",
  "allowJs": true,
  "skipLibCheck": true,
  "esModuleInterop": true,
  "allowSyntheticDefaultImports": true,
  "strict": true,
  "forceConsistentCasingInFileNames": true,
  "module": "esnext",
  "moduleResolution": "node",
  "resolveJsonModule": true,
  "isolatedModules": true,
  "noEmit": true
  },
  "include": ["./src/**/*"],    
  "exclude": ["./node_modules", "./public", "./dist", "./.vscode"]
}
Enter fullscreen mode Exit fullscreen mode

jest config:

module.exports = {
  roots: ['<rootDir>/src'],
  transform: {
    '\\.(ts|tsx)?$': 'babel-jest',
  },
  testMatch: ['<rootDir>/src/**/?(*.)test.{ts,tsx}'],   // looks for your test
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  testPathIgnorePatterns: ['/node_modules/', '/public/'],
  setupFilesAfterEnv: [
    'jest-dom/extend-expect',
    '@testing-library/react/cleanup-after-each'
  ]  // sets ut test files
};
Enter fullscreen mode Exit fullscreen mode

webpack config:

const path = require('path')

// Plugins
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: {
        dev: './src/index.tsx',
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'js/[name].bundle.js',
    },
    devServer: {
        compress: true,
        port: 3000,
        hot: true,
    },
    devtool: 'source-map',
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx'],
    },
    module: {
        rules: [
            /**
             * Gets all .ts, .tsx, or .js files and runs them through eslint
             * and then transpiles them via babel.
             */
            {
                test: /(\.js$|\.tsx?$)/,
                exclude: /(node_modules|bower_components)/,
                use: ['babel-loader'],
            },

            /**
             * All output '.js' files will have any sourcemaps re-processed by
             * source-map-loader.
             */
            { test: /\.js$/, enforce: 'pre', loader: 'source-map-loader' },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
        }),
    ],
}
Enter fullscreen mode Exit fullscreen mode

Extra resources:

Some resources that helped me learn different parts of using React testing library:

Docs:

https://testing-library.com/docs/react-testing-library/intro

Create-react-app: https://www.youtube.com/watch?v=Yx-p3irizCQ&t=266s

Testing redux: https://www.youtube.com/watch?v=h7ukDItVN_o&t=375s

Component unit testing: https://www.youtube.com/watch?v=KzeqeI046m0&t=330s

Mocking and more component testing: https://www.youtube.com/watch?v=XDkSaCgR8g4&t=580s

Portals: https://www.youtube.com/watch?v=aejwiTIBXWI&t=1s

Mocking: https://www.youtube.com/watch?v=9Yrd4aZkse8&t=567s

Test async components: https://www.youtube.com/watch?v=uo0psyTxgQM&t=915s

Discussion (9)

Collapse
skube profile image
skube

I believe the latest version of RTL has cleanup already baked-in.
Also, jest-dom has been scoped to @testing-library.

So, instead of adding the following to setupTests.js (or *.ts):

import '@testing-library/react/cleanup-after-each';
import 'jest-dom/extend-expect';

You should:

yarn remove jest-dom
yarn add -D @testing-library/jest-dom

And then only need to include the following in your setupTests.js (or *.ts):

import '@testing-library/jest-dom/extend-expect'
Collapse
abetoots profile image
abe caymo

also note (from docs):
cleanup is only done automatically if the testing framework you're using supports the afterEach global (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.

Collapse
rodolphonetto profile image
Rodolpho Netto

I having a lot of problems with import




It returns me Cannot find module './dist/extend-expect' from 'extend-expect.js'



even when it is installed =/

  "devDependencies": {
    "@testing-library/dom": "^6.4.0",
    "@testing-library/jest-dom": "github:testing-library/jest-dom",
    "@testing-library/react": "^9.1.4",
Collapse
skube profile image
skube • Edited on

I think you have it installed incorrectly, my package.json looks like:

 "devDependencies": 
    "@testing-library/jest-dom": "^4.1.0",
    "@testing-library/react": "^9.1.4",
...

Did you add jest-dom with:

yarn add -D @testing-library/jest-dom
Thread Thread
rodolphonetto profile image
Rodolpho Netto • Edited on

Yes, I dont know why it was installed like that... I've followed the docs, but now it is working fine, thank you :)

Collapse
bkjoel profile image
bkjoel

I think using “getting started” in the header is misleading.. just as you’ve said - “...this guide is a step by step walkthrough on how to set up your testing environment...”.

The “getting started” part is misleading people into thinking that they’ll learn how to actually test using these tools 🤷🏼‍♂️

Might be a good idea to change that part of the title to “setting up..”?

Collapse
aromanarguello profile image
aromanarguello Author

Thanks for the feedback, I think you are right! I just changed it to what you suggested :)

Collapse
bkjoel profile image
bkjoel

Thank you for taking my input politely ☺️

Collapse
instinct profile image
Instinct • Edited on

First of all, great article.
@aromanarguello I am having trouble with jest.config.js in create-react-app configuration.
It would be very nice if you could help me.

const { defaults } = require('jest-config');

module.exports = {
    ...defaults,
    testEnviroment: "jest-environment-jsdom-sixteen",
    automock: false,
    verbose: true,
}
Enter fullscreen mode Exit fullscreen mode

Why is this showing me an error whenever I am trying to do npm test?

error

This is my npm test command in package.json:

"test": "react-scripts test -- --config=jest.config.js"
Enter fullscreen mode Exit fullscreen mode