Introduction
Reliable JavaScript applications need more than functional features and a successful build. As an application grows, a small change in one function can unexpectedly affect another part of the system. Jest for unit testing in JavaScript applications provides a practical way to catch these issues early by testing individual pieces of application logic in isolation.
In our development workflow, we use Jest to verify business logic, validate expected behaviours, mock external dependencies, and identify regressions before code reaches production. Its built-in assertions, mocking capabilities, and coverage reporting make it suitable for both small projects and larger JavaScript and TypeScript codebases.
In this guide, we’ll walk through our approach to JavaScript unit testing with Jest, including project setup, test organization, practical Jest test cases, API mocking, code coverage, performance optimization, and mistakes we encountered while building and maintaining test suites.
1. Why We Use Jest for JavaScript Unit Testing
There are several JavaScript testing tools available today, but Jest has become a popular choice for teams that want an integrated testing experience.
1. Simple Configuration
One of the biggest advantages of the Jest testing framework is its relatively simple setup.
A basic project can start running tests with only a few configuration options. Jest can automatically discover test files based on common naming conventions such as:
*.test.js
*.spec.js
*.test.ts
*.spec.ts
This reduces the amount of testing infrastructure developers need to maintain themselves.
2. Fast Test Execution
Jest is designed to execute tests efficiently and can run tests in parallel across workers. This becomes especially useful when a project contains hundreds or thousands of tests.
Developers can also run only the tests related to recently changed files during development, while the complete suite can run in CI/CD.
3. Built-in Mocking and Assertions
Jest provides built-in functionality for creating mock functions, mocking modules, spying on calls, and making assertions.
For example:
const calculateDiscount = jest.fn();
calculateDiscount(100);
expect(calculateDiscount).toHaveBeenCalledWith(100);
There is no need to install a separate mocking library for many common testing scenarios.
4. Code Coverage Reporting
Jest can generate coverage information for statements, branches, functions, and lines.
Running:
npx jest --coverage
can provide a useful overview of which parts of an application are covered by tests.
Coverage should not be treated as the only measurement of test quality, but it can help identify areas that have been completely overlooked.
5. JavaScript and TypeScript Support
Jest can be used in JavaScript applications as well as TypeScript projects with the appropriate transformation or runtime configuration.
This makes it practical for teams gradually migrating JavaScript applications to TypeScript or maintaining mixed codebases.
Jest vs Mocha vs Vitest
| Feature | Jest | Mocha | Vitest |
| Test runner | Built-in | Built-in | Built-in |
| Assertions | Built-in | Usually paired with another library | Built-in |
| Mocking | Built-in | Often requires additional tools | Built-in |
| Coverage | Built-in integration | Additional setup commonly used | Built-in integration |
| TypeScript | Supported with configuration | Supported with configuration | Strong TypeScript/Vite integration |
| Configuration | Generally simple | Flexible but more manual | Very simple for Vite projects |
| Best fit | General JS/TS applications | Highly customizable testing stacks | Vite-based modern applications |
The right choice depends on the application and development ecosystem. Jest remains a strong option when a team wants a mature, integrated testing framework with a broad set of features.
2. Our Jest Setup and Test Structure
A good test suite is not only about writing assertions. Test organization becomes increasingly important as the application grows.
Installing and Configuring Jest
For a JavaScript project, Jest can be installed as a development dependency:
npm install --save-dev jest
A basic package.json script can then be added:
{
"scripts": {
"test": "jest"
}
}
Tests can be executed with:
npm test
For projects requiring additional configuration, a Jest configuration file can be created.
For example:
// jest.config.js
module.exports = {
testEnvironment: "node",
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.js"
]
};
The exact configuration should depend on the application. React applications, Node.js services, and TypeScript projects may require additional settings.
Recommended Folder and File Structure
We prefer keeping tests close to the code they validate when that makes the project easier to navigate.
For example:
src/
├── services/
│ ├── priceService.js
│ └── priceService.test.js
│
├── utils/
│ ├── validation.js
│ └── validation.test.js
│
├── components/
│ ├── ProductCard.jsx
│ └── ProductCard.test.jsx
│
└── test/
├── mocks/
└── fixtures/
Another valid approach is to keep all tests in a dedicated tests directory.
The important point is consistency. Developers should be able to find the tests for a piece of application code without searching through the entire repository.
Naming Conventions
Clear naming makes Jest test cases easier to understand.
We commonly use:
priceService.test.js
validation.test.js
ProductCard.test.jsx
Test names should describe behaviour rather than implementation.
Prefer:
it("returns the discounted price when a valid discount is provided", () => {
// ...
});
over:
it("calls calculateDiscount internally", () => {
// ...
});
The first test describes what the application should do. The second focuses on how the implementation currently works.
Separating Test Data, Mocks, and Test Cases
As the test suite grows, large amounts of test data can make individual test files difficult to read.
We therefore separate reusable fixtures and mocks when appropriate:
test/
├── fixtures/
│ └── products.js
└── mocks/
└── apiClient.js
This keeps the actual Jest test cases focused on setup, behaviour, and assertions.
Jest Testing Flow
A typical unit test follows this flow:
Application code
↓
Jest test
↓
Mock dependencies
↓
Assertion
↓
Test result
The objective is to isolate the unit being tested while replacing external dependencies that are not part of the current test.
3. How We Use Jest for Unit Testing in JavaScript Applications
The easiest way to understand How to use Jest for unit testing in JavaScript applications is to work through a practical feature.
Consider a price-calculation function for an e-commerce application.
Application Function
export function calculateFinalPrice(price, discountPercentage) {
if (price < 0) {
throw new Error("Price cannot be negative");
}
if (discountPercentage < 0 || discountPercentage > 100) {
throw new Error("Invalid discount percentage");
}
return price - (price * discountPercentage) / 100;
}
The function has several behaviours that should be tested:
-
A product with no discount
-
A product with a valid discount
-
A 100% discount
-
A negative price
-
An invalid discount percentage
Basic Unit Test
The simplest Jest test checks the expected result for a normal scenario:
import { calculateFinalPrice } from "./calculateFinalPrice";
describe("calculateFinalPrice", () => {
it("calculates the final price after applying a discount", () => {
const result = calculateFinalPrice(1000, 20);
expect(result).toBe(800);
});
});
This test follows a simple pattern:
Arrange → Act → Assert
We provide the input, execute the function, and verify the output.
Positive Scenario
We should also test different valid inputs:
it("returns the original price when there is no discount", () => {
expect(calculateFinalPrice(500, 0)).toBe(500);
});
it("calculates a 50 percent discount correctly", () => {
expect(calculateFinalPrice(2000, 50)).toBe(1000);
});
These tests verify expected behaviour under normal conditions.
Negative Scenario
Good unit testing should also verify how the application handles invalid input.
it("throws an error for a negative price", () => {
expect(() => calculateFinalPrice(-100, 10))
.toThrow("Price cannot be negative");
});
it("throws an error when discount is greater than 100", () => {
expect(() => calculateFinalPrice(1000, 120))
.toThrow("Invalid discount percentage");
});
Edge-Case Validation
Edge cases are often where bugs appear.
For example:
it("returns zero for a 100 percent discount", () => {
expect(calculateFinalPrice(1000, 100)).toBe(0);
});
it("handles a zero price", () => {
expect(calculateFinalPrice(0, 20)).toBe(0);
});
These tests make the expected boundaries explicit.
Organizing Jest Test Cases
When learning How to write and organize Jest test cases in real projects, grouping related behaviours with describe() makes the suite easier to understand.
describe("calculateFinalPrice", () => {
describe("valid inputs", () => {
it("calculates a discount correctly", () => {
// ...
});
it("returns the original price without a discount", () => {
// ...
});
});
describe("invalid inputs", () => {
it("rejects a negative price", () => {
// ...
});
it("rejects an invalid discount", () => {
// ...
});
});
});
This structure becomes particularly useful when a feature contains multiple related behaviours.
4. Mocking APIs and Dependencies
Unit tests should ideally focus on one unit of behaviour.
If the function being tested calls an API, database, file system, or external service, directly using that dependency can make the test slow and unreliable.
Jest provides several ways to isolate dependencies.
Mock Functions with jest.fn()
jest.fn() creates a mock function that can track calls and control return values.
const getUser = jest.fn();
getUser.mockReturnValue({
id: 1,
name: "John"
});
const user = getUser();
expect(user.name).toBe("John");
expect(getUser).toHaveBeenCalledTimes(1);
Mock functions are useful when the dependency itself does not need to be tested.
Mock Modules with jest.mock()
Suppose an application uses an API client:
import apiClient from "./apiClient";
export async function getProduct(id) {
const response = await apiClient.get(`/products/${id}`);
return response.data;
}
The API client can be mocked:
jest.mock("./apiClient");
import apiClient from "./apiClient";
import { getProduct } from "./getProduct";
it("returns product data from the API", async () => {
apiClient.get.mockResolvedValue({
data: {
id: 1,
name: "Laptop"
}
});
const product = await getProduct(1);
expect(product).toEqual({
id: 1,
name: "Laptop"
});
});
The test does not make a real HTTP request.
Testing Asynchronous Functions
Modern JavaScript applications frequently depend on asynchronous operations.
Jest supports async/await naturally:
it("handles a successful API response", async () => {
apiClient.get.mockResolvedValue({
data: {
id: 1,
name: "Laptop"
}
});
const result = await getProduct(1);
expect(result.name).toBe("Laptop");
});
Error scenarios should also be tested:
it("propagates an API error", async () => {
apiClient.get.mockRejectedValue(
new Error("Request failed")
);
await expect(getProduct(1))
.rejects
.toThrow("Request failed");
});
Keep Unit Tests Independent
A unit test should not depend on:
-
A real database
-
A live API
-
Another test running first
-
Shared mutable state
-
A particular execution order
Mocking external dependencies helps make tests deterministic and significantly easier to run in CI/CD environments.
5. Code Coverage and Test Performance
Jest code coverage gives developers visibility into how much of their code is exercised by tests.
A coverage report can measure:
-
Statements
-
Branches
-
Functions
-
Lines
For example:
npx jest --coverage
A report might show:
File % Stmts % Branch % Funcs % Lines
———————————————————–
priceService.js 100 90 100 100
validation.js 95 88 90 95
apiService.js 82 70 85 82
6. Focus on Meaningful Coverage Gaps
A high percentage does not automatically mean a test suite is good.
For example, a test might execute a line without actually verifying meaningful behaviour.
Instead of asking:
“How can we reach 100%?”
we should ask:
“Which important behaviours are not currently protected by tests?”
Branch coverage can be particularly useful because it can reveal untested error paths and conditional logic.
7. Avoid Chasing 100% Coverage Without Purpose
A project with 100% statement coverage can still contain bugs.
Tests should prioritize:
-
Business-critical logic
-
Error handling
-
Boundary conditions
-
Frequently changed code
-
Regression-prone functionality
-
Important user workflows
Coverage is a measurement tool, not the definition of test quality.
8. Keeping the Jest Suite Fast
As the number of Jest test cases increases, execution time can become a development concern.
Some practices that help include:
-
Mock external services.
-
Avoid unnecessary integration work inside unit tests.
-
Keep test setup lightweight.
-
Avoid excessive global setup.
-
Run targeted tests during development.
-
Remove redundant test cases.
-
Keep tests independent.
9. Benchmark: Before and After Optimization
A benchmark should ideally be taken from your own CI/local environment because execution time depends on hardware, Node.js version, Jest configuration, and project size.
For illustration, a team could track results like this:
| Metric | Before Optimization | After Optimization |
| Jest test cases | 420 | 420 |
| Execution time | 48 sec | 29 sec |
| Statement coverage | 78% | 86% |
| Branch coverage | 65% | 79% |
| Functions covered | 81% | 89% |
The important lesson is not simply reducing execution time. A useful optimization should make the test suite faster without sacrificing meaningful coverage or reliability.
10. Real Jest Mistakes We Encountered
Writing tests is not enough. The way tests are written can also introduce maintenance problems.
Here are some common issues we encountered.
1. Testing Implementation Details Instead of Behaviour
Tests that depend heavily on internal implementation can break whenever code is refactored—even if the application’s behaviour remains unchanged.
Prefer:
expect(result).toBe(800);
over testing every internal function call unless that interaction is itself important behaviour.
2. Using Too Many Mocks
Mocks are useful, but excessive mocking can make tests disconnected from reality.
If everything is mocked, a test may pass even though the actual pieces of the
application do not work correctly together.
A good rule is to mock external boundaries and dependencies that are irrelevant to the behaviour under test.
3. Writing Multiple Behaviours in One Test
A test should ideally verify one clear behaviour.
Avoid creating a test that simultaneously checks:
-
Authentication
-
User creation
-
Database updates
-
Email notifications
-
Response formatting
Breaking these behaviours into focused tests makes failures easier to understand.
4. Forgetting to Clear Mocks
Mock state can leak between tests when mocks are reused.
Depending on the situation, Jest provides:
jest.clearAllMocks();
or configuration options such as:
clearMocks: true
The correct approach depends on how the test suite is structured.
5. Creating Tests That Depend on Execution Order
Each test should be capable of running independently.
Bad pattern:
Test 1 creates data
↓
Test 2 expects Test 1’s data
Better:
Test 1 → creates its own data
Test 2 → creates its own data
This prevents tests from passing individually but failing when the entire suite runs.
6. Ignoring Error and Edge Cases
Testing only successful scenarios creates a false sense of confidence.
For each important function, consider:
-
What happens with valid input?
-
What happens with invalid input?
-
What happens when an external dependency fails?
-
What happens with empty input?
-
What happens at the minimum and maximum boundaries?
This is where many valuable Jest test cases come from.
11. Our Jest Unit Testing Checklist
Before considering a unit test complete, we use a checklist like this:
-
Tests one clear behaviour.
-
Uses a descriptive test name.
-
Covers the expected success scenario.
-
Covers relevant failure scenarios.
-
Covers important edge cases.
-
Mocks only external or unnecessary dependencies.
-
Does not depend on another test.
-
Does not depend on execution order.
-
Produces deterministic results.
-
Uses meaningful assertions.
-
Can run reliably in CI/CD.
-
Adds value rather than simply increasing coverage.
This checklist is especially useful during code reviews because it shifts the discussion from “Did we add tests?” to “Did we add the right tests?”
Conclusion
A strong unit-testing strategy is an important part of maintaining reliable JavaScript applications. Jest for unit testing in JavaScript applications gives development teams a practical combination of test execution, assertions, mocking, and coverage reporting without requiring a large testing infrastructure.
Our approach is to focus on behaviour, isolate external dependencies, test both expected and unexpected scenarios, and keep the test suite independent and fast. We also use Jest code coverage as a guide for finding gaps rather than treating a particular percentage as the definition of quality.
Whether you are introducing testing to an existing JavaScript application or building a new TypeScript project, JavaScript unit testing with Jest can provide an effective foundation for catching defects early and keeping production code reliable.
The goal is not to write the largest possible number of tests. The goal is to create a maintainable set of Jest test cases that gives developers confidence when they change, refactor, and ship the application.
FAQs
Create a test file and use Jest’s test() or it() function together with an assertion.
function add(a, b) {
return a + b;
}
test(“adds two numbers”, () => {
expect(add(2, 3)).toBe(5);
});
Run the test using:
npm testYou can mock an API module using jest.mock() and configure the mocked function to return the response required by the test.
jest.mock(“./apiClient”);
apiClient.get.mockResolvedValue({
data: {
id: 1
}
});
There is no universal percentage that guarantees good testing. A commonly used target is around 80% or higher, but teams should choose thresholds based on project risk and requirements.
More important than the percentage is whether critical business logic, failure scenarios, branches, and edge cases are properly tested.Jest is commonly used for unit and component-level testing, while Cypress is primarily used for browser-based end-to-end and application-level testing.
Jest can test an individual function such as:
calculateFinalPrice(1000, 20);
Cypress can test a broader user interaction, such as opening an application, filling out a form, submitting it, and verifying the result in the browser.
The two tools can complement each other rather than being direct replacements.This usually indicates that tests are sharing state or have another hidden dependency.
Common causes include:
-
Mocks not being cleared.
-
Shared mutable objects.
-
Global state leaking between tests.
-
Temporary files not being cleaned up.
-
Tests depending on execution order.
-
Asynchronous operations not being properly awaited.