What is Spy Stub Mock in Sinon

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.

Spy Stub Mock in Sinon-Spy Example oven and friend

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.

Spy Stub Mock in Sinon-Example Stub toy phone

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 in instagramService.js.
  • We define the getUserProfileController function directly in instagramController.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 the getUserProfile 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 using sinon.replace().
  • We call the getUserProfileController function from the controller, which internally calls the stubbed getUserProfile 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 the getUserProfile 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 the getUserProfile 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.