When you start building applications in Node.js, it is tempting to dive straight into writing features and only test them manually. While this can work for small projects, it quickly becomes a problem as your codebase grows. Bugs appear, existing features break when you add new ones, and manual testing becomes slow.
This is where test-driven development, or TDD, comes into play. TDD is a practice where you first write a test for a feature, watch it fail, then write the code to make it pass, and finally clean up your implementation while keeping the test green. This cycle encourages you to think clearly about what your code should do before you even write it.
In this article, we will walk through setting up a Node.js project for TDD, writing the first tests, and building a simple API with Jest and Supertest. By the end you will have a practical workflow that you can apply to your own projects.
Why TDD Matters in Node.js?
Node.js is often used for building backends and APIs. These systems typically interact with databases, handle multiple requests, and address edge cases such as invalid inputs or timeouts. If you rely only on manual testing, it is very easy to miss hidden bugs.
With TDD, you get:
Confidence that your code works as expected.
Documentation of your intended behavior through test cases.
Refactoring freedom, since you can change implementation details while ensuring nothing breaks.
Fewer regressions because tests catch mistakes early.
Let us start building a small project using this approach.
Step 1. Setting Up the Project
Create a new folder for the project and initialize npm:
mkdir tdd-node-example
cd tdd-node-example
npm init -yThis creates a package.json file that will hold project metadata and dependencies.
Now install Jest, which is a popular testing framework for Node.js:
npm install --save-dev jestAlso, install Supertest, which will help us test HTTP endpoints:
npm install --save-dev supertestTo make things easier, add a test script in package.json:
{
"scripts": {
"test": "jest"
}
}This allows you to run tests with npm test.
Step 2. Writing the First Failing Test
Let us create a simple module that manages a list of tasks, similar to a basic to-do list. Following TDD, we will start with the test.
Inside a tests folder, create taskManager.test.js:
const TaskManager = require("../taskManager");
describe("TaskManager", () => {
it("should add a new task", () => {
const manager = new TaskManager();
manager.addTask("Learn TDD");
const tasks = manager.getTasks();
expect(tasks).toContain("Learn TDD");
});
});We have not written taskManager.js yet, so that this test will fail. That is the point.
Run the test:
npm testJest will complain that ../taskManager it cannot be found. That confirms we need to write the implementation.
Step 3. Making the Test Pass
Now create taskManager.js at the root:
class TaskManager {
constructor() {
this.tasks = [];
}
addTask(task) {
this.tasks.push(task);
}
getTasks() {
return this.tasks;
}
}
module.exports = TaskManager;Run npm test again. This time the test passes. Congratulations, you just completed your first TDD cycle: red → green.
Step 4. Adding More Tests
Now, let us expand our tests. Modify taskManager.test.js:
const TaskManager = require("../taskManager");
describe("TaskManager", () => {
it("should add a new task", () => {
const manager = new TaskManager();
manager.addTask("Learn TDD");
expect(manager.getTasks()).toContain("Learn TDD");
});
it("should remove a task", () => {
const manager = new TaskManager();
manager.addTask("Learn Jest");
manager.removeTask("Learn Jest");
expect(manager.getTasks()).not.toContain("Learn Jest");
});
it("should return an empty list initially", () => {
const manager = new TaskManager();
expect(manager.getTasks()).toEqual([]);
});
});
Join the conversation! Your thoughts help the community grow.