Unit testing is a major topic for every developer. It is a fundamental practice in building software applications. Unit testing helps you to identify bugs early and makes code maintenance easier. By isolating and testing single units or components of your application, you can ensure their reliability and functionality.
When applying unit testing, you need to focus on the main logic of a component without affecting external dependencies or causing side effects—unintended changes that occur outside a function's scope, like database queries or network requests.
Jest is a popular testing framework that offers powerful capabilities to help in effective testing. Mocking in Jest helps you test and manage external dependencies and handle side effects.
In this guide, you will learn about unit testing essentials, focusing on Jest mocks. Whether you're just starting or looking to enhance your testing strategy, this guide will equip you with the knowledge to write effective and efficient tests.
Here's what we'll cover:
What is Unit Testing?
What are External Dependencies?
What are Side Effects?
What is Mocking?
Use Case: Login Express Controller
Summary
What is Unit Testing?
Unit testing is a software testing technique used to test a single component of your application in isolation. This component may be a class, a method, or a module.
Why You Should Use Unit Testing
You will be able to detect bugs earlier, it helps you to detect if a component behaves as expected.
Enables you to modify your component safely. If you update your component and, by mistake, add or modify something you should not, the test will fail if these changes introduce a new bug.
It can serve as a documentation that shows how individual units of your app work.
Encourages you to write cleaner code. The cleaner your component is, the easier and simpler your test will be.
It helps you to easily integrate different parts of your application, as you will be sure that every single component works correctly.
In the long term, you can maintain your application faster.
Let us dive-deep into some practical usages:
Let’s assume that you have a multiplication function that should take two arguments and return the result.
Here’s the code:
function multiply(a,b) {
return a*b
}
export default multiply
Note: To use Jest with Node.js ECMAScript modules, check out this article for configuration.
So how can you test this function using Jest?
Create __tests__ folder in the root folder.
Create file multiply.test.js inside __tests__ .
Note that any file ending with .test.js will be executed by Jest.
Start writing your tests by calling the
it("",()=>{})
Jest method.
Let's understand what `it("",()=>{})
` does:
The it
method is a Jest function used to test certain behaviors in your function.
The first argument should be the test name, which can be an assertion text for what you expect from this test.
For example, if you need to test whether the multiply
function returns the right result using the arguments and if they are numbers, you can write it("should return the multiplication of inputs of type number",()=>{})
.
The second argument is a function for your test logic. It gets invoked once you run your test.
To effectively write your tests, you should apply the AAA (Arrange-Act-Assert) Pattern.
Arrange: Setup the data or configure any dependencies you will use in this test.
Act: Call the function you are testing.
Assert: Write your expectations—how you are expecting the function you are testing to behave. For assertion, you will always use the
expect
Jest method.
Think of every it("",()=>{})
statement as a different scenario of your function.
Here’s an example:
import multiply from './../multiply.js'
it("should return the multiplication of inputs of type number", () => {
// Arrange
const testArg1 = 5;
const testArg2 = 2;
// Act
const result = multiply(testArg1, testArg2);
// Assert
expect(result).toBe(10);
});
it("should returns NaN if no arguments are passed", () => {
// Arrange
// Act
const result = multiply();
// Assert
expect(result).toBeNaN();
});
it("should returns NaN if only one argument is passed", () => {
// Arrange
const arg = 5;
// Act
const result = multiply(arg);
// Assert
expect(result).toBeNaN();
});
it("should returns Zero if one of the arguments is empty string", () => {
// Arrange
const testArg1 = "";
const testArg2 = 5;
// Act
const result = multiply(testArg1, testArg2);
// Assert
expect(result).toBe(0);
});
These tests are some of the tests you can add to your file. You can add more tests or eliminate some depending on the different scenarios of the function you are testing.
What are External Dependencies?
External dependencies are modules or functions that your code relies on, which originates outside your own codebase. These can include libraries, APIs, databases, functions or any service that your application interacts with.
Testing with external dependencies can be challenging because:
They can slow down tests due to network or processing delays.
They might not be available during the testing, which in turn causes failures.
As shown in the following function, what if your function calls another function? Most of the functions you write daily actually call other functions.
That is:
function processNumbers(numbers, callback) {
// numbers: array
// callback: function
return numbers.map(callback);
}
export default processNumbers;
When applying unit testing, units should be tested in isolation. processNumbers
function depends on another function callback
.
So what should you do in this case? Mocking is the solution and we’ll talk about it later in a different section.
What are Side Effects?
Side effects occur when a function modifies some state outside its own scope or has observable interactions with the outside world apart from returning a value.
Examples include modifying a global variable, changing a file system, or sending an HTTP request.
Side effects can make tests unpredictable and difficult to manage because they:
Might interact with other systems, causing alteration of external states.
Can lead to flaky tests if not isolated properly.
Here’s an example that returns a user from a database using their id
:
async function getUserFromDatabase(userId) {
// Simulates fetching from a database
return { id: userId, name: 'John' };
}
export {getUserFromDatabase}
Here’s another function that makes use of getUserFromDatabase
in the code above:
async function getProfile(userId) {
return await getUserFromDatabase(userId);
}
export default getProfile
While testing this function, you should not actually send a real request, all you need is to test the behavior of the getProfile
function without hitting any external system.
You can also use mocking to solve this situation.
What is Mocking?
Mocking is about simulation—you need to isolate a function that you are testing. If the function relies on any external dependency or may cause any side effect, you should simulate the behavior of those aspects.
Mocking involves creating a fake version of a function, object, or module to control its behavior during testing. This allows you to simulate different scenarios and verify interactions without relying on actual implementations.
We will focus on two approaches to mocking:
Function Mocks (also called Spies):
You can usejest.fn()
to create a mock function that can be used to track a function or replace real implementations. Or usejest.spyOn(object, methodName)
to track the calls ofobject[methodName]
.Module Mocks: You can use
jest.mock(“path-of-your-module”)
to mock entire modules or specific imports. By using it, all functions inside this module become mock functions. In addition, during testing, modules you are testing will receive a fake mocked version of this module.
Any mock function has methods that you can use to simulate the behavior of the function. Some of the most used methods are:
mockFn.mockImplementation(fn)
: Used to replace the actual implementation of a function.fn
is the replacement implementation.mockFn.mockReturnValue(value)
: You can use this if all you care about is the return value of a function.mockFn.mockResolvedValue(value)
: You can use this if the mock function returns a promise.
Example Usage 1
Let’s test processNumbers
by using function mocks. The challenge here is that processNumbers
takes a callback function as an argument. What if you need to test if this callback function get invoked inside processNumbers
?
Here’s the code:
import processNumbers from 'file-path';
test('processNumbers applies callback and return the right result', () => {
// Arrange
const arr = [2, 3]
const mockedCallback = jest.fn().mockImplementation(x => x + 2);
// Act
const result = processNumbers(arr, mockedCallback);
// Assert
expect(result).toEqual([4, 5]);
expect(mockedCallback).toHaveBeenCalledTimes(arr.length);
});
We started by arranging the arguments:
arr
variable is an array of numbers. We assigned it an array with random numbers in the test.The
callback
variable is a callback function. This function should be mocked in the test.
You may ask yourself why you should mock callback
, why not assign it as a normal function?
The answer is that, without mocking the callback
argument, you will not be able to track it inside processNumbers
while you are testing it. Because mocking creates a fake version of the function, it creates a spy that has a tracker through which you can assert any action taken in this mocked function, whether it gets called or any arguments are passed to it.
The jest.fn()
creates a mock function. You can pass a function to fn
in place of the real function.
Next, we “act” by calling the function we are testing: processNumbers
.
Finally, we wrote the assertions, which are expectations about how processNumbers
should behave and if processNumbers
applied callback
and returned the result.
Example Usage 2
Side effects are another aspect you need to handle in testing. In the getProfile
function, an external system is called, which calls a database to retrieve data, and this is a side effect.
In another scenario, a function may connect a database to create a user, and through testing you will not need to add or change actual data in the database.
To simulate the behavior of getUserFromDatabase
without actually hitting the database, you should mock its module, and by default, getUserFromDatabase
will be an empty mock function that can be tracked during your test.
Here’s the code:
import getProfile from 'file-path';
import { getUserFromDatabase } from 'file-path';
// Mock the module of getUserFromDatabase method
jest.mock('./../DB/databaseMethods.js');
describe('getProfile', () => {
it('should call getUserFromDatabase with the correct userId and return the result', async () => {
// Arrange
const userId = '123';
const dummyUser = { id: userId, name: 'John' };
getUserFromDatabase.mockResolvedValue(dummyUser);
// Act
const result = await getProfile(userId);
// Assert
expect(result).toEqual(dummyUser);
expect(getUserFromDatabase).toHaveBeenCalledWith(userId);
expect(getUserFromDatabase).toHaveBeenCalledTimes(1);
});
});
We started by arranging the arguments:
userId
is just a number.dummyUser
is an object that simulates a fake user data.We returned
dummyUser
fromgetUserFromDatabas
by usingmockResolvedValue
.
Similar to the last example, we “act” by calling the function being tested: getProfile
.
Finally, we wrote the assertions, you expectations about how getProfile
should behave and if the getUserFromDatabase
got called correctly and the result returned as expected.
Use Case: Login Express Controller
Here is a login controller that receives the email and password of a user through the req
object, and then searches for the user in the database. It does some checks, then returns a res
if everything is ok, or call next
with an error object.
import User from "file-path";
export const login = async (req, res, next) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return next(new Error("Invalid Email!"));
const checkPassword = user.checkPassword(password);
if (!checkPassword) return next(new Error("Invalid Password!"));
const token = user.generateToken();
return res.status(200).json({ success: true, results: { token } });
};
Think about the steps you can use to test the login function. You can ask some questions that’ll help you come up with ideas:
What are the scenarios of login
function workflow?
The user is not found.
Password is incorrect.
Everything is ok, and a response is returned with a token.
So you may assert login
to do the following:
login
should callnext
if user not found.login
should callnext
if password doesn't match.login
should call res.json with the token and call res.status with 200 if everything is ok.
What are the arguments that login
method should receive?
req
object withbody
property.res
object withstatus
andjson
property.next
function.
res.json()
or res.status()
or next()
all are functions that login
needs to do its work. During testing, you have no access to these arguments so you should mock them.
req
can be defined as{body: { email: "
test@foo.com
", password: "bar" }}
res
can be defined as{json: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis()}
next
can be defined asjest.fn()
Are there any interactions with external systems or any dependencies?
User.findOne()
user.checkPassword()
user.generateToken()
Thus, mocking is the solution:
For
User.findOne()
, you should mock the entireUser
module and set up the fakefindOne()
to return a fakeuser
. The challenge here is thatfindOne
is an object method. How can you track it?jest.spyOn(object, methodName)
is the soultion.
ThespyOn
method is used to track the calls ofobject[methodName]
, which, in our case, isUser.findOne
user.checkPassword()
anduser.generateToken()
should be mock functions.
To apply all of these concepts and put blocks with each other, the final test should be:
import User from "file-path";
import { login } from "file-path";
jest.mock("../DB/models/user.model.js");
let mockReq, mockRes, mockNext, dummyUser;
describe("login controller", () => {
beforeEach(() => {
mockReq = { body: { email: "test@foo.com", password: "bar" } };
mockRes = {
json: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
};
mockNext = jest.fn();
dummyUser = {
checkPassword: jest.fn(() => true),
generateToken: jest.fn(() => "token"),
};
});
it("should call next if user not found", async () => {
// Arrange
jest.spyOn(User, "findOne").mockResolvedValueOnce(null);
// Act
await login(mockReq, mockRes, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledWith(new Error("Invalid Email!"));
expect(mockRes.json).not.toHaveBeenCalled();
});
it("should call next if password doesn't match", async () => {
// Arrange
dummyUser.checkPassword.mockReturnValueOnce(false);
jest.spyOn(User, "findOne").mockResolvedValue(dummyUser);
// Act
await login(mockReq, mockRes, mockNext);
// Assert
expect(mockNext).toHaveBeenCalledWith(new Error("Invalid Password!"));
expect(dummyUser.generateToken).not.toHaveBeenCalled();
expect(mockRes.json).not.toHaveBeenCalled();
});
it("should call res.json with the token and call res.status with 200 if everything is ok", async () => {
// Arrange
jest.spyOn(User, "findOne").mockResolvedValue(dummyUser);
// Act
await login(mockReq, mockRes, mockNext);
// Assert
expect(mockNext).not.toHaveBeenCalled();
expect(User.findOne).toHaveBeenCalledWith({ email: mockReq.body.email });
expect(dummyUser.checkPassword).toHaveBeenCalledWith(mockReq.body.password);
expect(dummyUser.generateToken).toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({
success: true,
results: { token: "token" },
});
});
});
Final note: beforeEach
is a Jest hook, you can use it to implement some code before each test. Inside beforeEach
function, you can write any common variables your tests may need instead of writing them independently for each test.
Summary
In this tutorial you learned the basics of unit testing with Jest, focusing on how to use mocks. Unit testing helps ensure that individual parts of your code work correctly by testing them in isolation.
Handling external dependencies, managing side effects, and utilizing mocking are essential skills for robust testing. Jest provides powerful tools to address these challenges, making your tests more reliable, faster, and easier to maintain.
Understanding these concepts will help you write better tests and produce more resilient applications.
This tutorial explained how to use Jest’s mocking features to simulate external dependencies and manage side effects. It includes a practical example of testing an Express.js login controller, showing how to mock functions and control test scenarios.
This approach helps you create reliable tests and maintain code quality by isolating and managing dependencies effectively.