Think of Unit Testing as a software development technique where you break your software up into small, isolated units and write automated tests that ensure each unit works as expected.
In simpler terms, unit testing is about verifying that the smallest pieces of your application — usually functions — behave correctly under different conditions.
A unit can be:
- A function
- A method
- A component
- A utility
- A hook
- A service
The key idea is isolation.
You test one unit at a time, without depending on other parts of the system.
For example, instead of testing an entire application flow, you test a single function like:
function calculateTotal(price, quantity) {
return price * quantity;
}
Why Unit Testing is Required?
Unit testing is not just a best practice — it is a core part of professional software development, especially as applications grow in size and complexity.
Without tests, every change introduces risk.
With tests, you gain confidence.
Here are the real reasons why unit testing is required -
1) To Catch Bugs Early
Finding bugs early is significantly cheaper and easier than fixing them later in production.
Unit tests act as an automated safety net that quickly detects issues when code changes.
Instead of discovering a bug:
- During manual testing
- In production
- From a customer
You discover it immediately after running tests.
2) To Enable Safe Refactoring
As developers, we constantly refactor code to:
- Improve readability
- Optimize performance
- Reduce duplication
- Add new features
Without tests, refactoring is risky.
You don't know if something broke.
With tests, you can confidently modify code because the tests verify that behavior remains correct.
3) To Prevent Regression Bugs
A regression bug happens when a new change breaks existing functionality.
Unit tests prevent this.
Every time you run tests, you verify that previously working features still work.
This is critical in large applications where multiple developers are making changes simultaneously.
4) To Support Continuous Integration and Deployment (CI/CD)
Modern development workflows rely on automated testing.
Before code is deployed:
- Tests run automatically
- Failures block deployment
- Only stable code is released
Code That Feels Simple at the Start Doesn't Stay Simple
What begins as a small function or a quick feature often grows as new requirements are introduced. Edge cases appear, validations are added, and business logic evolves. Over time, multiple developers may contribute to the same code, increasing its complexity.
A function that once handled a single scenario may eventually need to support:
- Input validation
- Error handling
- Business rules
- Performance optimizations
- Backward compatibility
As complexity increases, the risk of introducing bugs also increases.
This is one of the key reasons unit testing becomes essential — it ensures that as code evolves, existing functionality continues to work as expected.
Unit Test Using Pure JavaScript
Before using testing frameworks like Jest or Vitest, it's important to understand that unit testing is simply verifying behavior, and this can be done using plain JavaScript.
Let's take a simple example -
function calculateTotal(price, quantity) {
return price * quantity;
}
function runTests() {
const tests = [
{ price: 100, quantity: 2, expected: 200 },
{ price: 50, quantity: 3, expected: 150 },
{ price: 0, quantity: 5, expected: 0 }
];
tests.forEach((test, index) => {
const result = calculateTotal(test.price, test.quantity);
if (result === test.expected) {
console.log(`✅ Test ${index + 1} Passed`);
} else {
console.log(`❌ Test ${index + 1} Failed`);
console.log("Expected:", test.expected);
console.log("Received:", result);
}
});
}
runTests();
What This Demonstrates
This is still a unit test, even though no library is used.
Because:
- We executed a function
- We compared the result with an expected value
- We reported whether the behavior was correct
Testing frameworks like Jest simply automate and organize this process, but the core idea remains the same.
Unit Test Using Jest
Jest is a popular JavaScript testing framework used to write and run unit tests in a structured and automated way. It provides built-in features such as test runners, assertions, mocking, and reporting, making it easier to verify that individual units of code behave as expected.
Instead of manually checking results using if statements and console.log, Jest allows developers to define tests using readable functions like test() and validate outcomes using assertions like expect().
In simple terms, Jest standardizes and automates the unit testing process.
Example: Unit Test Using Jest
Function Under Test
function calculateTotal(price, quantity) {
return price * quantity;
}
module.exports = calculateTotal;
Jest Test
const calculateTotal = require("./calculateTotal");
describe("calculateTotal", () => {
test("returns correct total for valid inputs", () => {
const result = calculateTotal(100, 2);
expect(result).toBe(200);
});
test("returns correct total when price is zero", () => {
const result = calculateTotal(0, 5);
expect(result).toBe(0);
});
test("returns correct total for another valid case", () => {
const result = calculateTotal(50, 3);
expect(result).toBe(150);
});
});
Unit testing with Jest follows a simple pattern:
Arrange → Act → Assert
- Arrange — Set up input values
- Act — Call the function
- Assert — Verify the result
What we did:
// 1) Importing the Function to Test
const calculateTotal = require("./calculateTotal");
// 2) Using describe() to Group Tests
describe("calculateTotal", () => {
// 3) Writing Individual Test Cases with test()
test("returns correct total for valid inputs", () => {
// 4) Executing the Function (Act Step)
const result = calculateTotal(100, 2);
// 5) Verifying the Result with expect()
expect(result).toBe(200);
How to Run Jest
Running Jest involves installing the package, writing tests, and executing them using a command. Once configured, Jest automatically discovers and runs all test files in your project.
Step 1: Install Jest
First, install Jest as a development dependency.
npm install --save-dev jest
Step 2: Add a Test Script
Update the scripts section in your package.json file.
{
"scripts": {
"test": "jest"
}
}
Step 3: Create a Test File
Jest automatically looks for files with names like:
- *.test.js
- *.spec.js
Example -
// calculateTotal.test.js
const calculateTotal = require("./calculateTotal");
test("calculates total correctly", () => {
expect(calculateTotal(100, 2)).toBe(200);
});
Step 4: Run Jest
Now execute the test runner using:
npm test
Jest will:
- Find all test files
- Run the tests
- Display results
Example output:
PASS calculateTotal.test.js
✓ calculates total correctly (2 ms)
Mocking Network Calls in Jest
In real-world applications, functions often depend on external services such as APIs, databases, or third-party systems. These dependencies can make tests slow, unreliable, or difficult to control.
Mocking solves this problem.
Mocking is the process of replacing real dependencies with controlled, simulated versions during testing. Instead of making an actual network request, we provide a fake response and verify how our code behaves.
This ensures that unit tests remain:
- Fast
- Predictable
- Independent
- Isolated from external systems
Example:
// priceService.js
async function fetchPrice() {
const response = await fetch("https://api.example.com/price");
const data = await response.json();
return data.price;
}
async function calculateTotal(quantity) {
const price = await fetchPrice();
return price * quantity;
}
module.exports = {
fetchPrice,
calculateTotal
};
Jest Test with the Mock:
const { calculateTotal, fetchPrice } = require("./priceService");
jest.mock("./priceService", () => ({
fetchPrice: jest.fn()
}));
describe("calculateTotal", () => {
test("returns correct total using mocked price", async () => {
// Arrange
fetchPrice.mockResolvedValue(100);
// Act
const result = await calculateTotal(2);
// Assert
expect(fetchPrice).toHaveBeenCalled();
expect(result).toBe(200);
});
});
What it does:
// 1) jest.mock() Replaces the Real Function
jest.mock("./priceService", () => ({
fetchPrice: jest.fn()
}));
// 2) mockResolvedValue() Controls the Response
fetchPrice.mockResolvedValue(100);
// above simulates:
return Promise.resolve(100);
// 3) Testing Behavior, Not the Network
expect(fetchPrice).toHaveBeenCalled();
expect(result).toBe(200);
Test Driven Development (TDD)
Test Driven Development (TDD) is a software development approach where tests are written before the actual implementation code. Instead of writing functionality first and testing later, developers define the expected behavior through tests and then write code to satisfy those tests.
The primary goal of TDD is to ensure correctness, improve design, and maintain confidence in the codebase as it evolves.
At the heart of TDD lies a simple, repeatable cycle known as:
Red → Green → Refactor
The Red, Green, Refactor Cycle
TDD follows a disciplined three-step workflow that guides development in small, incremental steps.
1) Red — Write a Failing Test 🔴
The first step in TDD is to write a test that defines the desired behavior of a feature or function. Since the functionality has not been implemented yet, the test will fail.
This failure is intentional and confirms that the test is valid and capable of detecting missing or incorrect behavior.
Example:
test("calculates total price correctly", () => {
expect(calculateTotal(100, 2)).toBe(200);
});
At this stage:
- The function may not exist yet
- The implementation is incomplete
- The test fails
This is the Red phase.
2) Green — Write the Minimum Code to Pass the Test 🟢
Next, we implement just enough code to make the test pass.
The focus here is correctness, not optimization or perfection.
Example:
function calculateTotal(price, quantity) {
return price * quantity;
}
Once the implementation satisfies the test, the test runner shows a successful result.
This is the Green phase.
3) Refactor — Improve the Code Safely 🔵
After the test passes, we improve the code while ensuring that behavior remains unchanged. This step focuses on code quality, readability, and maintainability.
function calculateTotal(price, quantity) {
if (price < 0 || quantity < 0) {
throw new Error("Invalid input");
}
return price * quantity;
}
Because tests already exist, we can refactor confidently.
If something breaks, the tests will immediately detect it.
This is the Refactor phase.
Test Coverage
Test coverage is a metric that measures how much of your application's code is executed when your tests run. It helps developers understand which parts of the codebase are tested and which parts are not.
In simple terms, test coverage answers the question:
"How much of my code is actually being tested?"
Test coverage does not guarantee that your code is bug-free, but it provides visibility into the areas that may need additional testing.
Why Test Coverage Matters
Test coverage helps teams maintain confidence in their codebase, especially as applications grow in size and complexity.
It is useful because it:
- Identifies untested code
- Reduces the risk of hidden bugs
- Improves code reliability
- Supports safe refactoring
- Provides measurable testing goals
For example, if a critical function is never executed during testing, test coverage will highlight it immediately.
How to Run Test Coverage in Jest
Jest provides built-in support for generating coverage reports.
Run Coverage
npm test -- --coverage
Example Coverage Output
| File | % Stmts | % Branch | % Funcs | % Lines |
|---|---|---|---|---|
| calculateTotal | 100 | 100 | 100 | 100 |
Where Coverage Reports Are Stored
After running coverage, Jest creates a folder:
coverage/
Inside it, you will find:
coverage/
└── lcov-report/
└── index.html
Final Note on Unit Testing in JavaScript
Unit testing is not just about writing tests — it's about building confidence in your code.
As applications grow, code changes frequently. New features are added, bugs are fixed, and logic evolves. Without tests, every change introduces uncertainty. With unit tests in place, developers gain a reliable safety net that ensures existing functionality continues to work as expected.
Unit testing also encourages better design. It pushes developers to write smaller, more focused functions, reduce unnecessary dependencies, and think clearly about expected behavior.
In the long run, unit testing saves time, reduces bugs, and improves code quality.
Write tests not because you have to — write tests because future you (and your team) will depend on them.
Top comments (0)