Setup up Jest & React Testing Library with Pre-Commits & CircleCI
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:
- Part One: Setting up TDD with Pre-Commits (Husky)
- Part Two: Setting up TDD with a CI/CD platform (CircleCI)
Tech Stack
- Setup tool: Vite ⚡️ with
react-ts
as a React Typescript template. - Main packages: Jest 🃏 & React Testing Library 🐙
- Pre-Commit tool: Husky 🐶
- 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:
- When we try to commit, Husky triggers the
pre-commit
script. - The
pre-commit
script runs Jest to execute all our tests. - If all tests pass ✅, the commit proceeds normally.
- 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:
- Creates a
pre-commit
script inside the.husky
folder like:.husky/pre-commit
- 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:
“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."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 ❌.
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.
Enough with failures, let’s try passing our tests ✅. Here, we’ve modified our code so that 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 ✅.
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:
node/install:
: Installs node.js withnode-version
16.13run:
: Checks if node.js is installed by running the commandnode --version
run:
: Installs all dependencies related to our project with thenpm install
command.run:
: Run tests by running the commandnpm 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”.
Now, select your remote repository service. My project repo is on Github, so i selected Github. You can choose between Github, Gitlab and Bitbucket.
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 🌁).
Then, click on “Create Project”. CircleCI will create your project ✅.
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 ✅.
Then, commit and push into main
branch in our Github.
See changes reflected on our Github. It should say “3 pending checks”.
See the CI pipeline in action in CircleCI. Click on the “test” job to see it in detail.
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.
We can see that all of our tests (2 tests: Link.test.tsx & Counter.test.tsx) ran and passed ✅.
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