DEV Community

Islam Elgohary
Islam Elgohary

Posted on • Updated on

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
  });
});
Enter fullscreen mode Exit fullscreen mode

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: 'user@mail.com',
         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');
  });

});
Enter fullscreen mode Exit fullscreen mode

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: 'user@mail.com',
         password: 'password123',
    };

    /** 
    * if a post request is sent to analytics.com/api/loggedIn with 
    * payload { email: 'user@mail.com' }, 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 'user@mail.com'
    * it will call analytics.com/api/loggedIn with payload { email: 'user@mail.com' }
    * 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');
  });

});
Enter fullscreen mode Exit fullscreen mode

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: 'user@mail.com',
         password: 'password123',
    };

    /** 
    * when function authenticate that exists in class User is called with
    * payload { email: 'user@mail.com', 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');
  });

});
Enter fullscreen mode Exit fullscreen mode

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.

Check my other articles at gohary.io

Top comments (0)