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
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`
Some conventions worth noting
Describe method:
describe('My component', () => {
// group of test()
})
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
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"
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
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'
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
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>
App.js
import React from 'react';
const App = () => <h1>Hi World</h1>;
export default App;
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
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()
]
};
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
};
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
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()
})
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
});
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"]
}
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
};
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',
}),
],
}
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
Top comments (9)
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):You should:
And then only need to include the following in your
setupTests.js
(or *.ts):also note (from docs):
cleanup
is only done automaticallyif 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.
I having a lot of problems with import
I think you have it installed incorrectly, my
package.json
looks like:Did you add
jest-dom
with:Yes, I dont know why it was installed like that... I've followed the docs, but now it is working fine, thank you :)
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..”?
Thanks for the feedback, I think you are right! I just changed it to what you suggested :)
Thank you for taking my input politely ☺️
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.
Why is this showing me an error whenever I am trying to do
npm test
?This is my
npm test
command inpackage.json
: