Using Stubs for Testing in JavaScript with Sinon.js

Introduction

Testing is a fundamental part of the software development process. When creating web applications, we make calls to third-party APIs, databases, or other services in our environment. Therefore, our tests must validate those request are sent and responses handled correctly. However, we may not always be able to communicate with those external services when running tests.

On our local development computer, we may not have the company API keys or database credentials to run a test successfully. That's why we sometimes "fake" the HTTP or database responses with a stub, tricking our code into behaving like a real request was made.

In this article, we will begin by looking at what stubs are and why we'd want to use them. We will then leverage Sinon.js, a popular JavaScript testing library, to create unit tests for JavaScript that stubs an HTTP request.

We'll then follow up on this with articles on Spies and Mocks:

What are Stubs?

A test stub is a function or object that replaces the actual behavior of a module with a fixed response. The stub can only return the fixed response it was programmed to return.

A stub can be seen as an assumption for our test - if we assume that an external service returns this response, this is how the function will behave.

Imagine that you have a function that accepts an HTTP request and gets data from a GraphQL endpoint. If we cannot connect to the GraphQL endpoint in our tests, we would stub its response so that our code will run as if GraphQL was actually hit. Our function code would not know the difference between an actual GraphQL response vs our stubbed response.

Let's look at scenarios where stubbing is useful.

Why Use Stubs?

While making requests to external services in a test, you can run into these problems:

  • Failing tests because of network connectivity errors instead of code errors
  • Long run times as network latency adds to the testing time
  • Mistakenly affecting production data with tests if a configuration error occurs

We can get around these problems by isolating our tests and stubbing these external service calls. There would be no network dependency, making our tests more predictable and less likely to fail. Without network latency, our tests are expected to be faster as well.

There are scenarios where external requests would not work. For example, it's common in CI/CD build processes to block external requests while running tests for security reasons. It's also likely that sometime we'll write code that depends on a service that's still in development and not in a state to be used.

In these cases, stubs are very useful as it allows us to test our code even when the service is unavailable.

Now that we know what stubs are and why they are useful, let's use Sinon.js to get practical experience with stubs.

Using Sinon.js to Create a Stub

We'll use Sinon.js to stub a response from a JSON API that retrieves a list of photos in an album. Our tests will be created with the Mocha and Chai testing libraries. If you would like to learn more about testing with Mocha and Chai before continuing, you can follow our guide.

Setup

First, in your terminal create a new folder and move into it:

$ mkdir PhotoAlbum
$ cd PhotoAlbum

Initialize NPM so you can keep track of the packages you install:

$ npm init -y

Once that is complete, we can begin to install our dependencies. First, let's install the request library, which will be used by our code to create an HTTP request to the API. In your terminal, enter:

$ npm i request --save

Now, let's install all the test libraries as dev dependencies. Test code is not used in production so we don't install testing libraries as a regular code dependencies with the --save option. Instead, we'll use the --save-dev option to tell NPM that these dependencies should only be used in our development/testing environment. Enter the command in your terminal:

$ npm i mocha chai sinon --save-dev

With all of our libraries imported, we'll create a new index.js file and add the code to make the API request there. You can use the terminal to create the index.js file:

$ touch index.js

In your text editor or IDE, write the code below:

const request = require('request');

const getPhotosByAlbumId = (id) => {
    const requestUrl = `https://jsonplaceholder.typicode.com/albums/${id}/photos?_limit=3`;
    return new Promise((resolve, reject) => {
        request.get(requestUrl, (err, res, body) => {
            if (err) {
                return reject(err);
            }
            resolve(JSON.parse(body));
        });
    });
};

module.exports = getPhotosByAlbumId;

This function makes a call to an API which returns a list of photos from an album whose ID is passed as a parameter to the function. We limit the response to only return three photos.

Now we'll write tests for our function to confirm that it works as expected. Our first test will not use stubs, but instead it'll make the actual request.

Testing Without Stubs

First, let's create a file to write our tests in. In the terminal or otherwise, make an index.test.js file in the current directory:

$ touch index.test.js

Our code will test that we get back three photos, and that each photo has the expected id, title, and url properties.

In the index.test.js file, add the following code:

const expect = require('chai').expect;
const getPhotosByAlbumId = require('./index');

describe('withoutStub: getPhotosByAlbumId', () => {
    it('should getPhotosByAlbumId', (done) => {
        getPhotosByAlbumId(1).then((photos) => {
            expect(photos.length).to.equal(3);
            photos.forEach(photo => {
                expect(photo).to.have.property('id');
                expect(photo).to.have.property('title');
                expect(photo).to.have.property('url');
            });
            done();
        });
    });
});

In this test, we first require the expect() function from Chai, and then require the getPhotosByAlbumId() function from our index.js file.

We use Mocha's describe() and it() functions so we can use the mocha command to run the code as a test.

Before running our test, we need to add a script to our package.json to run our tests. In the package.json file, add the following:

"scripts": {
    "test": "mocha index.test.js"
}

Now run your test with the following command:

$ npm test

You should see this output:

$ mocha index.test.js

  withoutStub: getPhotosByAlbumId
    ✓ should getPhotosByAlbumId (311ms)

  1 passing (326ms)

In this case, the test took 326ms to run, however that may vary depending on your Internet speed and location.

This test would not pass if you don't have an active internet connection since the HTTP request would fail. Although that does not mean that the function does not behave as expected. Let's use a stub so that we can test our function's behavior without a network dependency.

Testing with Stubs

Let's rewrite our function so that we stub the request to the API, returning a predefined list of photos:

const expect = require('chai').expect;
const request = require('request');
const sinon = require('sinon');
const getPhotosByAlbumId = require('./index');

describe('with Stub: getPhotosByAlbumId', () => {
    before(() => {
        sinon.stub(request, 'get')
            .yields(null, null, JSON.stringify([
                {
                    "albumId": 1,
                    "id": 1,
                    "title": "accusamus beatae ad facilis cum similique qui sunt",
                    "url": "https://via.placeholder.com/600/92c952",
                    "thumbnailUrl": "https://via.placeholder.com/150/92c952"
                },
                {
                    "albumId": 1,
                    "id": 2,
                    "title": "reprehenderit est deserunt velit ipsam",
                    "url": "https://via.placeholder.com/600/771796",
                    "thumbnailUrl": "https://via.placeholder.com/150/771796"
                },
                {
                    "albumId": 1,
                    "id": 3,
                    "title": "officia porro iure quia iusto qui ipsa ut modi",
                    "url": "https://via.placeholder.com/600/24f355",
                    "thumbnailUrl": "https://via.placeholder.com/150/24f355"
                }
            ]));
    });

    after(() => {
        request.get.restore();
    });

    it('should getPhotosByAlbumId', (done) => {
        getPhotosByAlbumId(1).then((photos) => {
            expect(photos.length).to.equal(3);
            photos.forEach(photo => {
                expect(photo).to.have.property('id');
                expect(photo).to.have.property('title');
                expect(photo).to.have.property('url');
            });
            done();
        });
    });
});

Before the test is run, we tell Sinon.js to stub the get() function of the request object that's used in getPhotosByAlbumId ().

The arguments passed to the yields() function of the stub are the arguments that will be passed to the callback of the get request. We pass null for the err and res parameters, and an array of fake photo album data for the body parameter.

Note: The after() function is run after a test is complete. In this case we restore the behavior of the request library's get() function. Best practices encourages our test states to be independent for each test. By restoring the function, the changes we made for this test would not affect how it is used in other tests.

Like before, we run this test with npm test. You should see the following output:

$ mocha index.test.js

  with Stub: getPhotosByAlbumId
    ✓ should getPhotosByAlbumId

  1 passing (37ms)

Great! Now without an internet connection, we are still sure that our function works well with the expected data. The test ran quicker as well! Without a network request, we simply need to get the data from memory.

Conclusion

A stub is a replacement for a function that returns fixed data when called. We usually stub requests to external systems to make test runs more predictable and eliminate the need for network connections.

Sinon.js can be used alongside other testing frameworks to stub functions. In this article, we stubbed an HTTP GET request so our test can run without an internet connection. It also reduced the test time as well.

If you would like to see the code for this tutorial, you can find it here.

In our next article, we continue on with Sinon.js and cover how to use spies for testing JavaScript.