Islam O. Elgohary
blog-post-2

Node Testing Essentials (A node developer's guide to testing)

Recently, I started to write complex tests for node and I realized that I need to use more than one library to effectively test node projects. However, I couldn't find a comprehensive guide for how to use those libraries together to create a robust test so I decided to share my experience to save you some time. Note that this is not a step-by-step tutorial but simply a guide to the tools and how to use them together.

Toolbox

First of all, allow me to introduce the tools I use for testing.

  1. Mocha: Framework for testing JS. Basically the skeleton of the tests.
  2. Chai: Assertions library with many useful plugins.
  3. Nock: A library that allows you to override the response of exact http requests with your own response.
  4. Sinon: Library for stubbing and mocking functions and objects.

Now let's get into more details about each tool.

1. Mocha

Mocha is the main testing framework. We use it to:

  1. Define test scenarios. (Using describe)
  2. Define test cases in each scenario. (Using it)
  3. Run tests using mocha command.

So for example, if we want to test the happy and sad cases of a login function, a minimal skeleton for the test might look like this:


describe('Login functionality', () => {
    it('should return authentication token', () => {
        // Logic to test success case
    });

    it('should return an error', () => {
        // Logic to test failure case
    });
});

In the above snippet we have a test scenario "Login functionality" that includes two test cases (1 success and 1 failure). Each of the cases includes the actual test logic (in our case, using chai, sinon and nock).

2. Chai

Chai provides many assertions for example, you can use assert to check that 2 values are equal: assert.equal(foo, 'bar'); You can also extend chai with plugins for example, Chai HTTP is a chai plugin that allows for testing http requests. Using it in our login example:


// server is an instance of the http server
describe('Login functionality', () => {
    it('should return authentication token', async () => {
    const credentials = {
            email: '[email protected]',
            password: 'password123',
    };

// send request to /login on our server
    const res = await chai.use(server)
        .post('/login')
        .set('Content-Type', 'application/javascript')
        .send(credentials);
// assert that the response is ok and that it has access_token
    assert.equal(res.statusCode, 200);
    assert.property(res.body, 'access_token');
    });

});

3. Nock

Let's assume that we want to test a function, however, the function itself makes http requests to another service, it doesn't make sense to make the test rely on whether the response of the other service is valid. Actually, it doesn't make sense to make the request at all while testing because that might affect the other service in an unwanted manner. That's why we have Nock. Nock allows you to override specific http requests and specify a specific response to them. Whenever the specified request is made during the test, the request is not sent but you receive the response that you specified.

To better understand the intuition of Nock, assume that our login function sends an http request including the user's email to another service that records the number of logged in users. In this case, we don't want to send the request or else it will record wrong data by adding one logged in user each time we run the tests. The code would look something like that:


// server is an instance of the http server
describe('Login functionality', () => {
    it('should return authentication token', async () => {
    const credentials = {
            email: '[email protected]',
            password: 'password123',
    };

    /** 
    * if a post request is sent to 
    * analytics.com/api/loggedIn with 
    * payload { email: '[email protected]' }, 
    * then don't send the request 
    * and respond with 200
    */
    nock('analytics.com', {
        reqheaders: {
        'content-type': 'application/json',
        },
        })
        .post('/api/loggedIn', {
            email: credentials.email,
        })
        .reply(200);
    /** 
    * when we call /login on our server 
    * with user email '[email protected]'
    * it will call analytics.com/api/loggedIn 
    * with payload { email: '[email protected]' }
    * which is the request nocked above
    */
    const res = await chai.use(server)
        .post('/login')
        .set('Content-Type', 'application/javascript')
        .send(credentials);

    assert.equal(res.statusCode, 200);
    assert.property(res.body, 'access_token');
    });

});

It is worth mentioning that nock matches exact requests which allows you to test that your function is sending the correct http request.

4. Sinon

You know how Nock mocks http requests? Sinon mocks functions. If you are testing function A which calls another function B, then you might need to mock function B's behavior and prevent it from being called. For example, assume that our login function calls a function "authenticate" from class "User" and we know that the function would fail with the credentials given in the test. Then we can use Sinon to stub this function and force it to succeed during the test:


describe('Login functionality', () => {
    it('should return authentication token', async () => {
    const credentials = {
            email: '[email protected]',
            password: 'password123',
    };

    /** 
    * when function authenticate that exists 
    * in class User is called with
    * payload {email: '[email protected]', password: 'password123'},
    * then don't call the function and instead 
    * return { success: true }
    */
    let stub = sinon.stub(User, 'authenticate');
    stub.withArgs(credentials).returns({ success: true });

    nock('analytics.com', {
        reqheaders: {
        'content-type': 'application/json',
        },
        })
        .post('/api/loggedIn', {
            email: credentials.email,
        })
        .reply(200);

    const res = await chai.use(server)
        .post('/login')
        .set('Content-Type', 'application/javascript')
        .send(credentials);

    assert.equal(res.statusCode, 200);
    assert.property(res.body, 'access_token');
    });

});

Conclusion

In this article, I have tried to create a concise guide for using Mocha, Chai, Nock and Sinon together to test node servers. I used a login endpoint as an example, however, I did not include all the implementation details because I wanted the article to be as short as possible focusing on using the tools together instead of how to use each tool. That being said, each of the 4 tools has a lot more functionality and use cases than what's mentioned in this article. you can know more about each one by reading the documentations.

Finally, I hope that this article will save you some time and effort and make it easier for you to start testing your projects.

Article on dev.to