Background

While talking about TDD within developing web systems, we cannot avoid testing RESTful APIs. I have seen some turtorials online (e.g. the articles which discuss about how to do unit test for REST API via leveraging certain npm packages under Nodejs environment) which mention that for Unit Test (hereafter, UT) it will hit database.

Well, in my humble option, in UT we should not hit database. The core idea of unit test is “Isolation“, i.e. isolate the function/methods under testing, presume everthing that the function/methods will interact with works as expected, and see if the function/methods generate the result we are hoping for. Therefore, if hitting database in UT, the testing result of the UT will depend on the status of the database, the coupling between the function/methods and the database makes the test is not a real “Unit Test”. Tests that requires access database should categories as Integration Test (hereafter IT) or End-to-End test. In the rest of this article, I will explain in details regarding how to do UT and IT for RESTful APIs with sourcecode samples provided under Nodejs environment.

Integration Test for RESTful API under Nodejs Environment: using MongoDB and supertest as an example

Assume that we want to test the following REST API: “POST /api/auth/signup“, we have the following code in our project:

app.js

1
2
3
4
import authRouter from './routes/auth-route.js';
...
app.use('/api/auth', authRouter);
...

auth-route.js

1
2
3
4
5
6
7
8
...
import {
signup,
signin,
} from '../controllers/auth-controller.js';
...
router.post('/signup', signup);
...

To test the api in integration test, we will need to complete two steps:

  1. connect to database for testing purpose only, not database in production environment
  2. test whether the api output expected results

1. Set a Mongodb connection selector between production environment and testing environment

app.js

1
2
3
4
5
6
7
8
9
10
11
12
// According to the value of process.env, choose the connection between prod env and test env
const mongodbConnect = process.env.NODE_ENV === "test"?
process.env.MONGODB_CLOUD_TEST : process.env.MONGODB_CLOUD_PROD;

const dbConnect = async () => {
try {
await mongoose.connect(mongodbConnect);
winstonLogger.info(`Connect to mongodb of ${process.env.NODE_ENV}`);
} catch (err) {
winstonLogger.error(`Failed to connect to mongodb due to ${err}`);
}
};

2. Using supertest to complete the test

auth-route.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe('POST signup', () => {
const tmpUser = {
name: 'testaa',
email: 'testaa@gmail.com',
password: '1234'
};

it('post a new user and respond with 200 and a msg showes that user has been created', async () => {
// a real call to the database in testing environment
const { text, status } = await request(app)
.post('/api/auth/signup')
.set('Accept', 'application/json')
.send(tmpUser);

expect(status).to.be.equal(200);
expect(text).to.be.equal('User has been created!')
});
...
});

As it can be seen from the above code snippet, the supertest makes a call to the database for creating a user profile, and the user profile is actually created in the database under test environment. Since the test involving testing the connection with a real database, I would like to categories this type of tests as integration test.

Then, how the unit test for the REST API should look like?

Unit Test for RESTful API under Nodejs Environment: using MongoDB and supertest + nock as an example

To clearly tell the difference between Integration Test and Unit Test, in this part I will still use supertest as the agent (alternative option can be axios, for example) to file a request to the “POST /api/auth/signup“, but in Unit Test, I will not let the request hitting datacase via http server, instead, I use nock to intercept the request and return the expected result, as shown blow:

describe('POST signup', () => {
  const tmpUser = {
    name: 'testaa',
    email: 'testaa@gmail.com',
    password: '1234'
  };

  it('post a new user and respond with 200 and a msg showes that user has been created', async () => {

    // file http request to the REST API
    const signupUser = async () => {
      return await request('http://test.com')
        .post('/api/auth/signup')
        .set('Accept', 'application/json')
        .send(tmpUser);
    };

    // mock http server: check if the server received expected params in request.body, then reply expected response 
    nock('http://test.com')
      .post('/api/auth/signup', body => {
        expect(body.name).to.be.equal('testaa');
        expect(body.email).to.be.equal('testaa@gmail.com');
        expect(body.password).to.be.equal('1234');
        return true;
      })
      .reply(200, 'User has been created!');

    const { text, status } = await signupUser();

    expect(status).to.be.equal(200);
    expect(text).to.be.equal('User has been created!')
  });
  ...
});

In the above unit test, I use nock to mock a http server. The supertest still file a request, but the request will be intercepted by the mock server and return expected results, instead of reaching a real server and further hitting a real database. This is the way how isolation is achieved: I am not coupling the test with a real database, based on the assumption that all other parts works okay (via mocking), everything is completed within the test method.

A further questions may be asked is that what is point to do a Unit Test like this? Should I include the unit tests in my project? The answer is Yes and No.

  • No: if you are developing a relatively small system as software vendor for a small business, or you are structuring a new software product that are targeting a small number of users, not aiming for millions or even billions users in the future, then I would say do not bother to add such unit test in your system. The ROI is not worth it, keep the integration test that check the connection with real database is enough.

  • Yes: if you are developing a system either big or small in a big company, or if you have a software product that is aiming for millions of daily users in the future (which means the product will be owned by a big company), I would say you probably will need to add the unit tests in your system. The reason? I would quote a saying from a movie called <Kingdom of Heaven>: nothing, everything.

Comment and share

  • page 1 of 1
Author's picture

Jingjie Jiang


Find a place I love the most