Spy Stub Mock in Sinon: Sinon.js is a powerful JavaScript library used for testing. It provides a variety of features to facilitate the testing process, including spies, stubs, mocks, and more.
Here’s a brief explanation of each:
Spy
A spy is a function that records information about its calls, such as arguments passed, the context in which it was called, and how many times it was called.
Spies can be used to assert that certain functions were called with specific arguments or to gather information about function calls for testing purposes without affecting the behaviour of the function itself.
Example: Pretend you have a little fairy friend hiding in the kitchen. Every time your mom or dad puts a cookie in the oven, the fairy quietly watches and counts how many cookies are baking. Later, you can ask the fairy how many cookies were baked.
Stub
A stub is a replacement for a function that allows you to control its behavior during testing.
Stubs are typically used to simulate the behavior of dependencies in order to isolate the code being tested.
They can be used to force a function to return a specific value, throw an error, or execute a custom function instead of its original implementation.
Example:Imagine you have a magic wand that can make the toy phone do whatever you want. With the wand, you can make the toy phone say “ring ring!” whenever you press the button, even if it doesn’t usually do that.
Mock
A mock is similar to a stub in that it replaces a function with a controlled implementation during testing.
However, mocks also include expectations about how the function should be called, such as the number of times it should be called or the specific arguments it should receive.
Mocks are often used to verify that functions are called correctly within the context of a test.
Example: Imagine you’re playing the role of the oven. Your mom or dad tells you exactly how many cookies they’re putting in and how long they should bake. So, when they put the cookies in, you have to make sure to bake them for the right amount of time and let them know if anything goes wrong.
Read Also : 2 Powerful Testing Suits: sinon and chai using nodejs
Spy Stub Mock in Sinon Example with Code
Use Case
Let’s create a simple example with a service and a controller for an Instagram-like application, and then we’ll use Sinon.js to create a spy, stub, and mock to monitor the behavior of the service function. We’ll keep it simple, assuming the service fetches user data.
First, let’s create the service file:
// instagramService.js
async function getUserProfile(username) {
// Assume this function fetches user profile data from Instagram API
// Here, we'll just return a mocked user profile object for demonstration
return {
username: username,
followers: 1000,
posts: 50
};
}
module.exports = { getUserProfile };
Now, the controller file:
// instagramController.js
const { getUserProfile } = require('./instagramService');
async function getUserProfileController(username) {
try {
const userProfile = await getUserProfile(username);
return userProfile;
} catch (error) {
console.error('Error fetching user profile:', error);
throw error;
}
}
module.exports = { getUserProfileController };
Spy Test
// test/instagramController.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const { getUserProfileController } = require('../instagramController');
const { getUserProfile } = require('../instagramService');
describe('Instagram Controller', () => {
describe('getUserProfileController', () => {
it('should fetch user profile data from InstagramService', async () => {
// Create a spy to monitor the getUserProfile function of InstagramService
const spy = sinon.spy(getUserProfile);
// Call the getUserProfileController function with a username
const username = 'john_doe';
const userProfile = await getUserProfileController(username);
// Assert that the getUserProfile function of InstagramService was called with the correct arguments
expect(spy.calledOnceWith(username)).to.be.true;
// Assert that the userProfile object returned by InstagramService is correct
expect(userProfile).to.deep.equal({
username: 'john_doe',
followers: 1000,
posts: 50
});
// Restore the original method to avoid affecting other tests
spy.restore();
});
});
});
Code Explanation
In this example:
- We define the
getUserProfile
function directly ininstagramService.js
. - We define the
getUserProfileController
function directly ininstagramController.js
. - The test file uses Sinon.js to create a spy on the
getUserProfile
function from the service. - We call the
getUserProfileController
function from the controller and ensure it behaves correctly by checking if it calls thegetUserProfile
function from the service with the correct arguments and returns the expected user profile object.
Stub Test
let’s modify the test to use a stub instead of directly spying on the getUserProfile
function. We’ll create a stub for the getUserProfile
function from the service module.
// test/instagramController.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const { getUserProfileController } = require('../instagramController');
const { getUserProfile } = require('../instagramService');
describe('Instagram Controller', () => {
describe('getUserProfileController', () => {
it('should fetch user profile data from InstagramService', async () => {
// Create a stub for the getUserProfile function of InstagramService
const stub = sinon.stub().resolves({
username: 'john_doe',
followers: 1000,
posts: 50
});
// Replace the original function with the stub
sinon.replace(getUserProfile, 'getUserProfile', stub);
// Call the getUserProfileController function with a username
const username = 'john_doe';
const userProfile = await getUserProfileController(username);
// Assert that the stub was called with the correct arguments
expect(stub.calledOnceWith(username)).to.be.true;
// Assert that the userProfile object returned by InstagramService is correct
expect(userProfile).to.deep.equal({
username: 'john_doe',
followers: 1000,
posts: 50
});
// Restore the original method to avoid affecting other tests
stub.restore();
});
});
});
Code Explanation
In this test:
- We create a stub using
sinon.stub()
and specify that it resolves to a predefined user profile object. - We replace the original
getUserProfile
function from the service module with the stub usingsinon.replace()
. - We call the
getUserProfileController
function from the controller, which internally calls the stubbedgetUserProfile
function. - We assert that the stub was called with the correct arguments and that the returned user profile object is correct.
- Finally, we restore the original method to avoid affecting other tests.
Mock Test
Let’s modify the test to use a mock instead of directly stubbing the getUserProfile
function. We’ll create a mock to verify that the getUserProfile
function from the service module is called with the correct arguments.
// test/instagramController.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const { getUserProfileController } = require('../instagramController');
const { getUserProfile } = require('../instagramService');
describe('Instagram Controller', () => {
describe('getUserProfileController', () => {
it('should fetch user profile data from InstagramService', async () => {
// Create a mock for the getUserProfile function of InstagramService
const mock = sinon.mock(getUserProfile);
const username = 'john_doe';
// Set an expectation on the mock
mock.expects('getUserProfile').once().withArgs(username).resolves({
username: 'john_doe',
followers: 1000,
posts: 50
});
// Call the getUserProfileController function with a username
const userProfile = await getUserProfileController(username);
// Assert that the mock was called as expected
mock.verify();
// Assert that the userProfile object returned by InstagramService is correct
expect(userProfile).to.deep.equal({
username: 'john_doe',
followers: 1000,
posts: 50
});
// Restore the original method to avoid affecting other tests
mock.restore();
});
});
});
Code Explanation
In this test:
- We create a mock using
sinon.mock()
on thegetUserProfile
function from the service module. - We set an expectation on the mock using
mock.expects()
to specify that the function should be called once with specific arguments and should resolve to a predefined user profile object. - We call the
getUserProfileController
function from the controller, which internally calls thegetUserProfile
function from the service module. - We verify that the mock was called as expected using
mock.verify()
. - We assert that the returned user profile object is correct.
- Finally, we restore the original method to avoid affecting other tests.
Conclusion
In this example, we demonstrated how to utilize Sinon.js, a powerful testing library, to test a Node.js application that interacts with an external service.
We focused on testing a controller function (getUserProfileController
) that relies on a service function (getUserProfile
).
- We started by employing a spy to monitor the behavior of the
getUserProfile
function, ensuring that it was called with the correct arguments from within the controller. This allowed us to observe the interaction between the controller and the service without affecting the actual behavior of the service function. - Next, we explored using a stub to replace the original
getUserProfile
function with a predefined behavior. This approach enabled us to isolate the controller function for testing, ensuring that its functionality was tested independently of the service implementation. - Additionally, we demonstrated the usage of a mock to set expectations on the
getUserProfile
function, verifying that it was called with the correct arguments. This approach facilitated testing the interaction between the controller and the service, ensuring that the controller function behaved as expected.
By leveraging Sinon.js’s capabilities, we were able to write comprehensive tests that verify the behaviour of our application under various scenarios. Employing spies, stubs, and mocks allowed us to isolate components, observe interactions, and set expectations, leading to more robust and maintainable code.
Hey Tech Enthusiasts!
I’m Avinash, a passionate tech blogger with over 13+ years of experience in the trenches of software engineering.
You could say I’ve worn many hats in my journey – from full-stack developer crafting beautiful and functional applications to Solution Architect, designing the architecture for complex systems.
Over the years, I’ve delved into a vast arsenal of languages and tools, including the Generative AI (LLMs, LLM-Proxy, Observability, Prompt Engineering), .NET family (.NET, .NET Core), PHP, Rust, Python, the JavaScript frameworks (Angular, React, Node.js), and databases like MySQL, SQL Server, MongoDB.
As the cloud revolutionized our world, I’ve become well-versed in both Azure and GCP platforms, wielding Docker for containerization and CI/CD pipelines to streamline development workflows.
Here on my blog, I aim to share the knowledge I’ve accumulated and the lessons I’ve learned along the way. Whether you’re a seasoned developer or just starting your coding adventure, I want to provide you with insightful, practical articles that tackle real-world tech challenges.
Get ready to explore the latest advancements, delve into programming concepts, and discover efficient solutions to your development dilemmas. So, buckle up, tech enthusiasts – let’s embark on this exciting journey together!