Setup up Jest & React Testing Library with Pre-Commits & CircleCI

Surajan Shrestha
readytowork, Inc.
Published in
9 min readApr 3, 2024

Let’s set up a Jest & React Testing Library with Pre-Commits (Husky) & CI (CircleCI) for a proper Test Driven Development (TDD).

TDD involves not only writing tests and making sure our application runs as it’s expected but also making sure it ships 🚢 with our CI/CD pipelines & has a good developer experience which can be provided using Pre-commit hooks.

Things we’re doing

This article is divided into two parts:

  1. Part One: Setting up TDD with Pre-Commits (Husky)
  2. Part Two: Setting up TDD with a CI/CD platform (CircleCI)

Tech Stack

  1. Setup tool: Vite ⚡️ with react-ts as a React Typescript template.
  2. Main packages: Jest 🃏 & React Testing Library 🐙
  3. Pre-Commit tool: Husky 🐶
  4. CI (Continuous Integration) platform: CircleCI 🚢

Folder Structure

This is the folder structure we’re going for:

-.circleci <= CI setup using CircleCI
-.husky <= Pre-Commit setup using Husky
-src
-components
-Counter
-index.tsx
-Counter.test.tsx <= Test for Counter Component
-Link
-index.tsx
-Link.test.tsx <= Test for Link Component
-__snapshots__ <= Snapshot created by Snapshot test
-other stuff...
-package.json
-node_modules & other stuff...

Please ignore the test files (Counter.test.tsx & Link.test.tsx)for now. I’ve written them just for the demo. They might not be perfect 😅.

1. Part One: Setup TDD with Pre-Commits (Husky)

Pre-commit hooks are special scripts that run in Git before a commit is made. We use such hooks to allow commits to only happen when certain conditions are met. This promotes better code quality and reduces unnecessary commits.

Husky 🐶 is a go-to tool for handling & setting up pre-commit hooks. This is how it works:

  1. When we try to commit, Husky triggers the pre-commit script.
  2. The pre-commit script runs Jest to execute all our tests.
  3. If all tests pass ✅, the commit proceeds normally.
  4. If any tests fail ❌, Husky prevents the commit, and we’ll see error messages detailing the failures.

This helps ensure code quality by catching errors before pushing changes.

a. Install Husky

Install husky using a package manager of your choice. I’m using npm. We’re installing Husky as a dev dependency.

npm install --save-dev husky

b. Initialize Husky

Run the husky init command on our root directory.

npx husky init

It does two things:

  1. Creates a pre-commit script inside the .husky folder like: .husky/pre-commit
  2. Updates or adds prepare script in our package.json. Inside the scripts section in our package.json, we’ll find: “prepare”: “husky”

c. Setup test scripts in package.json

In our package.json, in the “scripts” section let’s add our test scripts:

  1. “test”: “react-scripts test” : This is the default way to run tests. This will run our tests in watch mode 👀 i.e. if we change something in our code and hit save, all of our tests will re-run.
  2. "test:staged": "CI=true react-scripts test --o" : This runs tests in CI mode, which is more suitable to integrate with pre-commit hooks ✅.
    The — o flag is used to run tests related with only those files that have changed since last commit 😎.
// package.json
"name": "jest-react",
// ... usual stuff
"scripts": {
// ... usual stuff
"test": "react-scripts test",
"test:staged": "CI=true react-scripts test --o",
"prepare": "husky"
},

d. Setup tests to run before each commit

In our ./husky/pre-commit file, write the command npm run test:staged which will run the “test:staged” script defined in our package.json.

npm run test:staged

If the tests pass ✅, the commit is allowed to proceed. Else, if the tests fail ❌, the commit is aborted and we’ll see error messages explaining why.

d. Why did we use “test:staged” instead of “test” in our pre-commit 🤔?

If we use “test”: “react-scripts test” inside our ./husky/pre-commit file, Jest will get stuck on watch mode ⚠️, whenever we add a new commit, regardless of if our tests had passed or failed.
So, this approach is not suitable for pre-commits ❌.

But, if we use “test:staged”: “CI=true react-scripts test --o”, Jest understands not to run tests in watch mode due to the CI=true command. The --o flag ensures that only files changed since the last commit are tested, speeding up development, particularly in large projects.
So, this approach is suitable for pre-commits ✅.

e. Demo

Let’s mess up our code knowingly so that some of our tests fail ❌.

Changed code to make sure that tests fail ❌

Then, let’s add a new git commit git commit -m “testing husky”. We can see that our tests have failed ❌. Therefore, our commit does not get registered and gets discarded by Husky.

Commit get’s discarded as Tests have failed ❌

Enough with failures, let’s try passing our tests ✅. Here, we’ve modified our code so that tests pass ✅.

Changed code to make sure tests pass ✅

Now, let’s add a new commit git commit -m “testing husky, tests should pass”. We can see that all of our tests pass ✅. Therefore, our commit is successfully registered ✅.

Commit is registered as Tests have passed ✅

2. Part Two: Setup TDD with a CI/CD platform (CircleCI)

We’ve successfully setup Jest with Pre-Commit. Now, we setup in a CI/CD platform. We’ve chosen CircleCI for this.

When running tests with pre-commit, it’s better to run tests for only those files that have changed since last commit.

In a CI/CD environment 🚢, the best practice is to always run all tests so that our application is error prone ✅.

a. Write a script to run all tests in CI mode

In our package.json, let’s write another script that will run in CircleCI. The script is “test:staged_all”: “CI=true react-scripts test” .

This script “test:staged_all”, will run tests in CI mode (like we did in test:staged in Husky setup) but, it will run all the tests ⏳ rather than run tests for only those files that have changed since last commit.

// package.json
"name": "jest-react",
// ... usual stuff
"scripts": {
// ... usual stuff
"test": "react-scripts test",
"test:staged": "CI=true react-scripts test --o",
"test:staged_all": "CI=true react-scripts test",
"prepare": "husky"
},

b. Setup CircleCI Locally

In our root directory, create a .circleci folder and inside it, create a config.yml file.

CircleCI generally creates a .circleci/config.yml file automatically when you create a project into a new git branch, but for the purpose of this article, we’re going to manually create it.

Put this into your .circleci/config.yml file:

# Couldn't automatically generate a config from your source code.
# This is a generic template to serve as a base for your custom config

# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/configuration-reference
version: 2.1

# Node.js Orb
orbs:
node: circleci/node@5.0.2

# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/jobs-steps/#jobs-overview & https://circleci.com/docs/configuration-reference/#jobs
jobs:
test:
# Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub.
# See: https://circleci.com/docs/executor-intro/ & https://circleci.com/docs/configuration-reference/#executor-job
docker:
# Specify the version you desire here
# See: https://circleci.com/developer/images/image/cimg/base
- image: cimg/base:current

# Add steps to the job
# See: https://circleci.com/docs/jobs-steps/#steps-overview & https://circleci.com/docs/configuration-reference/#steps
steps:
# Checkout the code as the first step.
- checkout
# Install Node.js
- node/install:
node-version: "16.13"
- run: node --version
# Install Dependencies
- run:
name: Install dependencies
command: npm install
# Run Tests
- run:
name: Run tests
command: npm run test:staged_all
build:
docker:
- image: cimg/base:current
steps:
- checkout
# Replace this with steps to build a package, or executable
- run:
name: Build an artifact
command: touch example.txt
- store_artifacts:
path: example.txt
deploy:
docker:
- image: cimg/base:current
steps:
# Replace this with steps to deploy to users
- run:
name: deploy
command: "#e.g. ./deploy.sh"

# Orchestrate jobs using workflows
# See: https://circleci.com/docs/workflows/ & https://circleci.com/docs/configuration-reference/#workflows
workflows:
example:
jobs:
- test
- build:
requires:
- test
- deploy:
requires:
- test

In line 25 of the config.yml file, under jobs:>test:>steps:, we have the following steps:

  1. node/install: : Installs node.js with node-version 16.13
  2. run: : Checks if node.js is installed by running the command node --version
  3. run: : Installs all dependencies related to our project with the npm install command.
  4. run: : Run tests by running the command npm run test:staged_all

You can ignore rest of the jibberish 🙇‍♂️.

c. Push changes to Github

CircleCI has been setup locally. Now, let’s commit and push our code to Github 🌐.

d. Signup with CircleCI

Go to CircleCI, and sign up for an account. If you already have one, then log into it.

e. Create a new project

In your CircleCI dashboard, go to Projects and click on “Create Project”.

Create a new project in CircleCI

Now, select your remote repository service. My project repo is on Github, so i selected Github. You can choose between Github, Gitlab and Bitbucket.

Select your remote repository service

Confirm some details about your project. Give a Project Name, mine is article-tdd-jest. Follow the instructions to generate a Private SSH Key, use the public SSH key as a deploy key in our project’s repo in Github and copy the private key to add in the field given below (Image 1 🌁).

Alert ⚠️: Now, if you haven’t given CircleCI permissions to your project, you will not see your repository’s name in the Repository dropdown given below.

Solution ✅: For that, just click on the link: Update your GitHub App repo permissions and you’ll be redirected in CircleCI App in Github.
Go to Repository Access section, select “Only select repositories” > select your repo that you need CircleCI to give access to. Mine’s article-tdd-jest (Image 2 🌁).

Image 1: Confirm Details about your project in CircleCI
Image 2: Give CircleCI access to your remote repository

Then, click on “Create Project”. CircleCI will create your project ✅.

Create project on CircleCI

f. See CircleCI in action

Now, lets push some changes in Github to see CircleCI running its Continuous Integration (CI) pipeline automatically and also running our tests.

Lets update Counter.test.tsx so that it passes it’s tests ✅.

Updating Counter.test.tsx so that it passes it’s tests ✅

Then, commit and push into main branch in our Github.

Commit changes and push into main branch

See changes reflected on our Github. It should say “3 pending checks”.

Check changes reflected on Github

See the CI pipeline in action in CircleCI. Click on the “test” job to see it in detail.

Click on “test” to see details

We can see all the actions running that we’d defined in our .circleci/config.yml file like: “Install Node.js 16.13”, “node — version”, “Install dependencies” and “Run tests”.

Click on “Run Tests” to see tests in detail.

See all the actions defined in our “.circleci/config.yml” file run

We can see that all of our tests (2 tests: Link.test.tsx & Counter.test.tsx) ran and passed ✅.

See test details by clicking on “Run tests”

Thank you for coming this far 🙇‍♂️. I’ll forever be grateful for giving my article your valuable time and energy. Be sure to leave claps 👏 and comment if you have any queries.

Happy Coding!

Quote: “If you are not willing to be a fool, you can’t become a master.” — Jordan B. Peterson

--

--

Surajan Shrestha
readytowork, Inc.

A Software Engineer 💻 | Codes in 👨‍💻: JavaScript, TypeScript & Go | Stacks 🧠: React, React Native, Next.js, Node, Express, Go-Gin, SQL, NoSQL and AWS.