Skip to main content
Cookies 🍪

This site uses cookies to deliver and enhance the quality of its services and to analyze traffic as specified in the Privacy policy. If you agree, you'll help me to understand which type of content is more interesting for my audience.

Mirco Bellagamba

Testing Library Recipes - Getting Started

Automated software testing has become a critical organization process within software development to ensure that expected business systems and product features behave correctly as expected. When developing a React.js front-end application, the React Testing Library is the officially recommended tool and it is the primary choice for many developers because it encourages good practices by enforcing not to test implementation details, but by focusing on tests that closely resemble how your web pages are interacted by the users.

This is the very first article of a series talking about best practices in testing front-end applications using the React Testing Library. Even if you are not a React.js developer, you can find useful information because the underlying concepts are the same of the Core Testing Library.

If you are not familiar with the theory of software testing, or you don't know the meaning of concepts like unit test, integration test, stub, mock, test doubles, you should take a look to some Software Testing reference, just to make sure to speak the same language.

The best place to start learning how to test a React web application is probably the official documentation:

Although the official documentation is great, I found myself too many times digging the web for the perfect setup, trying to understand in which way my tests will be robust and give me confidence about the code I wrote. My journey with Testing Library started two years ago and since that time I widely experimented its features and its limits. I want to share this experience and my personal test recipes.

At the end of the article, I share with you a repository that you can use as reference or as template to setup your project.

Let's start simple from the foundation concepts.

Basic concepts

An automated test is just a code checking the correctness of another piece of code. But how should you write this code? A common way to setup tests is the Arrange-Act-Assert pattern: a pattern for arranging and formatting code in UnitTest methods.

  1. Arrange all necessary preconditions and inputs.
  2. Act on the object or method under test.
  3. Assert that the expected results have occurred.

For example, this code is a simple test.

function sum(numbers: number[]): number {
	return numbers.reduce((partial, current) => partial + current, 0);
}

function shouldSumAllNumbers() {
	// Arrange
	const input = [1, 2, 3];

	// Act
	const output = sum(input);

	// Assert
	if (output !== 6) {
		throw new Error(`Test failed. Expected: 6, Actual: ${output}.`);
	}
}

If you're asking... Yes, it's not very different from the "sum test" you have probably already seen on every other introductory resource on testing 😴. I promise to talk about more interesting stuff later on. Even if not required, as I showed previously, writing and executing tests is way easier using frameworks or a set of testing utilities, especially for writing more complex tests, as those involving the DOM. So, let's set up our test environment.

Setup the environment

Depending on your project setup, you'll need some initial configuration to run tests on your React application.

  1. Install required dependencies
  2. Setting up the testing framework
  3. Start testing!

Projects created with Create React App have out of the box support for React Testing Library, you can skip to fine tuning. Projects created with other tool chains like Next.js, Gatsby.js needs this steps. If you are a complete beginner, it's probably better to pick CRA and start testing without caring about configuration. If you are setting up a custom toolchain then these steps could be really useful.

This guide makes some assumptions:

project-root/       // The root directory
 |-src/             // Contains the JS/TS source code
 |-test/            // Contains test config and utilities
   |-config/        // Contains test config files
   |-setupTests.js // The test env setup file

If you use a different setup, this guide could still work but you probably need to tweak some pieces, such as file paths. If you need a more advanced setup, you could check out Jest - Using with webpack.

1. Install dependencies

First of all, let's install required npm packages.

npm i -D jest babel-jest @testing-library/jest-dom @testing-library/react @testing-library/user-event

What have we just installed?

2. Configure Jest

Jest aims to work out of the box, config free, on most JavaScript projects. But in spite of this, I prefer to customize the configuration to support these 3 features.

  1. Add support for testing library and TS files.
  2. Stub file imports
  3. Stub CSS imports

Jest config file

Create a jest.config.js file in the project root directory.

module.exports = {
	verbose: true,
	roots: ["<rootDir>/src"],
	collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
	setupFilesAfterEnv: ["<rootDir>/test/setupTests.js"],
	testMatch: [
		"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
		"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}",
	],
	testEnvironment: "jsdom",
	transform: {
		"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
		"^.+\\.css$": "<rootDir>/test/config/cssTransform.js",
		"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)":
			"<rootDir>/test/config/fileTransform.js",
	},
	transformIgnorePatterns: [
		"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
		"^.+\\.module\\.(css|sass|scss)$",
	],
	moduleFileExtensions: [
		"web.js",
		"js",
		"web.ts",
		"ts",
		"web.tsx",
		"tsx",
		"json",
		"web.jsx",
		"jsx",
		"node",
	],
	resetMocks: true,
};

This configuration file instruct Jest about:

Jest setup file setupTests.js

Create a setupTests.js file in /test/.

import "@testing-library/jest-dom";

It instructs Jest with Testing Library custom matchers.

CSS transformer

Create the file /test/config/cssTransform.js.

"use strict";

module.exports = {
	process() {
		return "module.exports = {};";
	},
	getCacheKey() {
		// The output is always the same.
		return "cssTransform";
	},
};

This is a custom Jest transformer turning style imports into empty objects. In our tests, we does not need to import real CSS files.

File transform

Create the file /test/config/fileTransform.js.

"use strict";

const path = require("path");
const camelcase = require("camelcase");

module.exports = {
	process(src, filename) {
		const assetFilename = JSON.stringify(path.basename(filename));

		if (filename.match(/\.svg$/)) {
			const pascalCaseFilename = camelcase(path.parse(filename).name, {
				pascalCase: true,
			});
			const componentName = `Svg${pascalCaseFilename}`;
			return `const React = require('react');
      module.exports = {
        __esModule: true,
        default: ${assetFilename},
        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
          return {
            $$typeof: Symbol.for('react.element'),
            type: 'svg',
            ref: ref,
            key: null,
            props: Object.assign({}, props, {
              children: ${assetFilename}
            })
          };
        }),
      };`;
		}

		return `module.exports = ${assetFilename};`;
	},
};

Importing real file assets is something we do not care in testing. This custom Jest transformer is responsible for:

Start testing your components

Now that Jest is installed and configured we can set up the test script. In your package.json add or update the test script to run jest. There's no need of additional command line parameters since the configuration file take care of the customizations.

// package.json
{
	"scripts": {
		"test": "jest"
	}
}

Run tests in watch mode launching npm test -- --watch (or npx jest --watch) in the command line.

Now our test environment is ready 🙌. Let's write our first test.

Given this App component:

function App() {
	return (
		<div>
			<h1>Testing Library Recipes</h1>
			<a href="https://testing-library.com/">Getting Started</a>
		</div>
	);
}
export default App;

This test ensures that the page renders a link.

import { render, screen } from "@testing-library/react";
import App from "./App";

it("Should contain a link", () => {
	render(<App />);
	const linkElement = screen.getByRole("link", { name: /getting started/i });
	expect(linkElement).toBeInTheDocument();
});

The test does not rely on any implementation detail but it makes assumptions only on what final users actually see, as states the guiding principle of Testing Library.

The more your tests resemble the way your software is used, the more confidence they can give you.

Running npm test the console output should be like the following.

> jest

 PASS  src/App.test.tsx
  ✓ Should contain a link (71 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.726 s
Ran all test suites.

Bonus: Run tests on commit

A test environment is really effective only if tests run frequently. The best way to do that is to set up a Continuous Integration server that automatically run tests at every push. Besides that, it could be useful to run tests even before each commit. This give you a faster feedback, and it prevents you from committing not working code. Husky is a powerful tool that helps us to configure Git hooks to achieve this result.

  1. Let's install and init Husky in our project! This command installs Husky as dev dependency and it adds a prepare script in our package.json.
npx husky-init && npm install

You should have a new prepare script in your package.json. If you don't see it, add it manually.

// package.json
{
	"scripts": {
		"prepare": "husky install"
	}
}
  1. Install husky hooks running the prepare script (or you can directly run npx husky install).
npm run prepare
  1. Then we need to create a Git pre-commit hook. This pre-commit hook runs npm test just before the commit.
npx husky add .husky/pre-commit "npm test"

If npm test command fails, your commit will be automatically aborted.

As your test suite grows, the execution time could slow down your development process. Running all tests in your local machine before every single commits could become a waste of time. In that case, running tests on a Continuous Integration server is probably the better choice. However, it shouldn't be an all or nothing approach: you can keep fast tests on your pre-commit hook (unit tests) and move slower ones on the CI process (end-to-end tests).

GitHub Actions

GitHub Actions provide an easy way to automate software workflows, including Continuous Integration, and it is free for public repositories. Setting up a GitHub Action that run tests on push is really common workflow, and GitHub suggests a Node.js template for this if you switch to the Actions tab on your GitHub repository page. However, you can set up it manually and achieve the same result even before pushing your code to GitHub. For this CI action, GitHub needs a workflow configuration file, which defines the environment and the commands to run.

To get started quickly, create a node.js.yml file in .github/workflows directory of your repository. The file content should be like this.

name: Node.js CI

on:
  push:
    branches: [$default-branch]
  pull_request:
    branches: [$default-branch]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x, 14.x, 15.x]

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js $NaN
        uses: actions/setup-node@v1
        with:
          node-version: $NaN
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

Remember to replace $default-branch with the name of your default branch (es. main / master).

This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node. For more information see Using Node.js with GitHub Actions. This template will fit most use cases, but you can customize the CI process depending on your needs. You can read more on this directly on Github Actions Docs.

Wrapping up

Getting ready for testing requires these steps:

  1. Install Jest, Testing Library and all the required dependencies
  2. Configure Jest
  3. Configure Git hooks
  4. Set up a GitHub Action

I leave you with a project template that you can use as reference. This is a custom development toolchain which includes React Testing Library, Jest, Husky, TypeScript, Babel, Webpack, React.

https://github.com/mbellagamba/testing-library-recipes

Happy testing! 😃