Categories: node js, unit testing

API Unit Testing with Node JS

Reading Time: 3 minutes

Unit testing is a software development process in which different parts of an application, called units, are individually and independently tested for correct operation. Tests ensure the quality of the code and the fact that it functionscorrectly. External developers can also easily get to know your code baseby consulting the tests.

Unit testing should be enforced for the development of a system via a practice called Test Driven Development (TDD) which involves creating your tests before the functionality is implemented. This brings three benefits:

  • Obliges the developer to write the unit tests
  • Provides a method of testing the software whilst it is being developed
  • Proof to other people and continuous integration platforms that the software works

Unit testing can be measured within a project by the level of coverage. Code coverage is a collection of metrics which allow you to analyse how much of your code has been tested. Tools such as Istanbul provide a method of analysing the branch, statement and function coverage. It is important to note that it does not validate the quality of your unit tests. Why? Because you may not validate critical attributes of a given payload for instance.

Example interface of Istanbul

When implementing new functionality, unit tests can be separated into two separate areas:

  • Testing functionality via the HTTP request (one per feature is sufficient)
  • Testing small functions individually (all of the if statements and feature flags)

A recommendation into certain items to test are as follows:

  • The response of a function with a certain environment variable configuration e.g. feature x activated (check rollbacks still work if a feature on prod is not currently activated)
  • The response of a HTTP Route (Status and response body)
  • Check metrics are logs are called
  • Database queries (when the database is working / not working) this should involve querying the database after a function / http route has been called to update / delete / create data
  • Broker messages (check the publication of a message and the fact it was received / not received by a consumer)

describe('[API] POST /games/:id/play', () => {
  before(async () => {
    // start web server
    app = await web.start();
      
    // connect to database
    await MongoClient.connect(database.url, database.options);
  });
  after(async () => {
     // disconnect web server
     await web.stop();
     // disconnect database
     await MongoClient.disconnect(database.url, database.options);
  });
  beforeEach(async () => {
    // clean up database
    await Games.collection().remove({})
  });
  
  it('should win a game', async () => {
     // insert a game fixture (so that one exists)
     await Games.collection().insert(getGameFixtures().games.one);
     
     // perform the http post request to the api
     const { body, status } = await request(app)
     .post('/api/games/000000000001/play'
     .send({
       id: 000000000000,
       email: 'tom@tom.com'
     });
    
    // asserting using chai the result and the http status
    expect({ body, status }).to.deep.equal({
      status: 200,
      body: { result: 'won' }
    });
});

In the above example we create one unit test with a structure we can adhere to which are follows:

  • Before all the tests execute initialise the database and web server
  • Before each test make sure nothing is in the database (as each test adds fixtures to the database)
  • After the tests within this feature have been run close the web server and database
  • Always deeply assert your findings from a response? Why because there should be no excuse for any additional attributes or ones which you can not assert
  • Avoid nesting assertions to improve readability

To expand on the test above to test another use case there should be no real reason to perform another HTTP Request (as this can slow down your tests), below is an example:

it('should not win a game if it does not exist', async () => {
    const { result } = await playGame({
       id: 000000000000,
       email: 'tom@tom.com'
     });
    
    // asserting using chai the result and the http status
    expect(result).to.deep.equal({ result: 'lost' });
});

In order to test an error, it is good practice to introduce a try catch and avoid nested assertions. Why? Because then the code is more readable. Assertions are easier to read when they are indented at the same level and grouped together at the end of the test definition (after the function calls).

An error example:

 
it('throws an error on process start', async () => {
  let error;
  try {
    await process.start();
  } catch (err) {
    error = err;
  }

  expect(error).to.be.instanceof(Error);
});

We may want to test an element in a function that are not returned directly by the response. An example of this could be logging a warning within the application code. For this we can use the library Sinon to assert that a certain scenario was produced by using a spy.

it('should not win a game if it does not exist', async () => {
     sandbox.spy(logger, 'warn');
     const { result } = await playGame({
       id: 000000000000,
       email: 'tom@tom.com'
     });
    
    expect(result).to.deep.equal({ result: 'lost' });
    expect(logger.warn.args).to.deep.equal([[{
      id: 000000000000,
      email: 'tom@tom.com'
    }]]);
});

We may also want to change the default comportement in a test by forcing a function to return something in particular. Lets take an example, imagine a function returns the current date, to test and assert this it would be impossible as the date is constantly changing e.g. never the same date twice. We could use Sinon to stub a function in this case.

function getDate() {
   return new Date()
};
module.exports = getDate;
const utils = require(../getDate);
it('should return a date', async () => {
     const date = new Date(2017, 12, 4);
     sandbox.stub(utils, 'getDate').returns(date);
     const dateResult = getDate();
     expect(dateResult).to.deep.equal(date);
});

Introducing this line of code within our tests allows us to always assert that the date is ‘Thu Jan 04 2018’ even when the test is run the day after.

It is always a good idea to make your test definition description easily understandable, some good examples are as follows:

  • Should create a new user | should not create a new user due to one already existing with the same email

It can be seen that there are a variety of ways to test API Code. All functions and external library calls can be tested in one way or another. Even external API’s can be mockedwith libraries such as NockThere should be no reason why a code base cannot have 100% testing coverage. A good tip is that if a code is hard to test, it is probably badly written.

Please follow and like us:

Leave a Reply

Your email address will not be published. Required fields are marked *

*