DEV Community

Cover image for Unit and integration testing for Node.js apps
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Unit and integration testing for Node.js apps

Written by Andrew Evans✏️

With any application, testing is an integral part of the development process.

Building tests with your application enables you to:

  • Quickly verify that changes to a project do not break expected behavior
  • Act as pseudo documentation as path flows are documented
  • Easily demonstrate application behaviors
  • Quickly take a review of your application’s health and codebase

This post is going to introduce unit and integration testing of Node.js applications.

We’re going to be reviewing my Express.js API ms-starwars, which is on GitHub here. I recommend doing a git clone of my project and following along as I discuss different ways to unit test the application.

LogRocket Free Trial Banner

An overview of testing

When testing with Node.js, you’ll typically use the following:

The term testing also typically refers to the following:

  • unit testing – testing your application code and logic. This is anything that your code actually does and is not reliant upon external services and data to accomplish.
  • integration testing – testing your application as it connects with services inside (or outside) of your application. This could include connecting different parts of your application, or connecting two different applications in a larger umbrella project.
  • regression testing – testing your application behaviors after a set of changes have been made. This is typically something you do before major product releases.
  • end-to-end testing – testing the full end-to-end flow of your project. This includes external HTTP calls and complete flows within your project.

Beyond these four, there are also other forms of testing specific to applications and frameworks.

In this post, we’re going to focus on unit and integration testing.

First, let’s discuss the different frameworks we’ll be using.

What is mocha?

Mocha is a test runner that enables you to exercise your Node.js code. It works well with any Node.js project, and follows the basic Jasmine syntax similar to the following (borrowed from the mocha getting started docs.

describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal([1, 2, 3].indexOf(4), -1);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

With mocha, you can also include the use of assertion libraries like assert, expect, and others.

Mocha also has many features within the test runner itself. I highly recommend reading A quick and complete guide to Mocha testing by Glad Chinda for more information.

What is chai and chai-http?

Chai offers an assertion library for Node.js.

Chai includes basic assertions that you can use to verify behavior. Some of the more popular ones include:

  • should
  • expect
  • assert

These can be used in your tests to evaluate conditions of code you’re testing, such as the following borrowed from chai’s homepage:

chai.should();

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
tea.should.have.property('flavors')
  .with.lengthOf(3);
Enter fullscreen mode Exit fullscreen mode

Chai-http is a plugin that offers a full-fledged test runner that will actually run your application and test its endpoints directly:

describe('GET /films-list', () => {
  it('should return a list of films when called', done => {
    chai
      .request(app)
      .get('/films-list')
      .end((err, res) => {
        res.should.have.status(200);
        expect(res.body).to.deep.equal(starwarsFilmListMock);
        done();
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

With chai-http, the test runner starts your application, calls the requested endpoint, and then brings it down all in one command.

This is really powerful and helps with integration testing of your application.

What is sinon?

In addition to having a test runner and assertions, testing also requires spying, stubbing, and mocking. Sinon provides a framework for spys, stubs, and mocks with your Node.js tests.

Sinon is fairly straightforward, and you just use the associated spy, stub, and mock objects for different tests in your application.

A simple test with some stubs from sinon would look like this:

describe('Station Information', function() {
  afterEach(function() {
    wmata.stationInformation.restore();
  });
  it('should return station information when called', async function() {
    const lineCode = 'SV';
    const stationListStub = sinon
      .stub(wmata, 'stationInformation')
      .withArgs(lineCode)
      .returns(wmataStationInformationMock);
    const response = await metro.getStationInformation(lineCode);
    expect(response).to.deep.equal(metroStationInformationMock);
  });
});
Enter fullscreen mode Exit fullscreen mode

I know there’s a lot going on here, but lets just pay attention to this:

const stationListStub = sinon
      .stub(wmata, 'stationInformation')
      .withArgs(lineCode)
      .returns(wmataStationInformationMock);
Enter fullscreen mode Exit fullscreen mode

This is creating a stub for the wmata service’s method stationInformation with args lineCode that will return the mock at wmataStationInformationMock.

This lets you build out basic stubs so that the test runner will use your stubs in lieu of methods it runs across. This is good because you can isolate behavior.

Sinon can do a lot more than just stubs.

For more on testing with sinon, I recommend reading How to best use Sinon with Chai by Leighton Wallace.

Demo

Before I dive into actually building tests, I want to give a brief description of my project.

ms-starwars is actually an orchestration of API calls to the Star Wars API (SWAPI), which is available here. SWAPI is a very good API unto itself, and provides a wealth of data on a large part of the Star Wars cannon.

What’s even cooler is that SWAPI is community-driven. So if you see somewhere missing information, you can open a PR to their project here and add it yourself.

When you call endpoints for SWAPI, the API returns additional endpoints you can call to get more information. This makes the rest calls somewhat lightweight.

Here’s a response from the film endpoint:

{
    "title": "A New Hope",
    "episode_id": 4,
    "opening_crawl": "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....",
    "director": "George Lucas",
    "producer": "Gary Kurtz, Rick McCallum",
    "release_date": "1977-05-25",
    "characters": [
        "https://swapi.co/api/people/1/",
        "https://swapi.co/api/people/2/",
        "https://swapi.co/api/people/3/",
        "https://swapi.co/api/people/4/",
        "https://swapi.co/api/people/5/",
        "https://swapi.co/api/people/6/",
        "https://swapi.co/api/people/7/",
        "https://swapi.co/api/people/8/",
        "https://swapi.co/api/people/9/",
        "https://swapi.co/api/people/10/",
        "https://swapi.co/api/people/12/",
        "https://swapi.co/api/people/13/",
        "https://swapi.co/api/people/14/",
        "https://swapi.co/api/people/15/",
        "https://swapi.co/api/people/16/",
        "https://swapi.co/api/people/18/",
        "https://swapi.co/api/people/19/",
        "https://swapi.co/api/people/81/"
    ],
    "planets": [
        "https://swapi.co/api/planets/2/",
        "https://swapi.co/api/planets/3/",
        "https://swapi.co/api/planets/1/"
    ],
    "starships": [
        "https://swapi.co/api/starships/2/",
        "https://swapi.co/api/starships/3/",
        "https://swapi.co/api/starships/5/",
        "https://swapi.co/api/starships/9/",
        "https://swapi.co/api/starships/10/",
        "https://swapi.co/api/starships/11/",
        "https://swapi.co/api/starships/12/",
        "https://swapi.co/api/starships/13/"
    ],
    "vehicles": [
        "https://swapi.co/api/vehicles/4/",
        "https://swapi.co/api/vehicles/6/",
        "https://swapi.co/api/vehicles/7/",
        "https://swapi.co/api/vehicles/8/"
    ],
    "species": [
        "https://swapi.co/api/species/5/",
        "https://swapi.co/api/species/3/",
        "https://swapi.co/api/species/2/",
        "https://swapi.co/api/species/1/",
        "https://swapi.co/api/species/4/"
    ],
    "created": "2014-12-10T14:23:31.880000Z",
    "edited": "2015-04-11T09:46:52.774897Z",
    "url": "https://swapi.co/api/films/1/"
}
Enter fullscreen mode Exit fullscreen mode

Additional API endpoints are returned for various areas including characters, planets, etc.

To get all the data about a specific film, you’d have to call:

  • the film endpoint
  • all endpoints for characters
  • all endpoints for planets
  • all endpoints for starships
  • all endpoints for vehicles
  • all endpoints for species

I built ms-starwars as an attempt to bundle HTTP calls to the returned endpoints, and enable you to make single requests and get associated data for any of the endpoints.

To setup this orchestration, I created Express.js routes and associated controllers.

I also added a cache mechanism for each of the SWAPI calls. This boosted my APIs performance so that these bundled HTTP calls don’t have the latency associated with making multiple HTTP calls, etc.

Within the project, the unit tests are available at /test/unit. The integration tests are available at test/integration. You can run them with my project’s npm scripts:

npm run unit-tests and npm run intergration-tests.

In the next sections, we’ll walk through writing unit and integration tests. Then we’ll cover some considerations and optimizations you can make.

Let’s get to the code.

Unit tests

First, let’s create a new file in the sample project at /test/firstUnit.js

At the top of your test, let’s add the following:

const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;
const swapi = require('../apis/swapi');
const starwars = require('../controllers/starwars');
// swapi mocks
const swapiFilmListMock = require('../mocks/swapi/film_list.json');
// starwars mocks
const starwarsFilmListMock = require('../mocks/starwars/film_list.json');
Enter fullscreen mode Exit fullscreen mode

What’s this doing? Well, the first several lines are pulling in the project’s dependencies:

const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;
const swapi = require('../apis/swapi');
const starwars = require('../controllers/starwars');
Enter fullscreen mode Exit fullscreen mode
  • Pulling in the sinon framework.
  • Pulling in the chai framework.
  • Defining expect so we can use it assertions.
  • Pulling in the swapi api service that are defined in the project. These are direct calls to the SWAPI endpoints.
  • Pulling in the starwars api controllers that are defined in the project. These are orchestration of the SWAPI endpoints.

Next, you’ll notice all the mocks pulled in:

// swapi mocks
const swapiFilmListMock = require('../mocks/swapi/film_list.json');
// starwars mocks
const starwarsFilmListMock = require('../mocks/starwars/film_list.json');
Enter fullscreen mode Exit fullscreen mode

These are JSON responses from both the SWAPI endpoints and results returned from the project’s controllers.

Since our unit tests are just testing our actual code and not dependent on the actual flows, mocking data enables us to just test the code without relying on the running services.

Next, let’s define our first test with the following:

describe('Film List', function() {
  afterEach(function() {
    swapi.films.restore();
  });
  it('should return all the star wars films when called', async function() {
    sinon.stub(swapi, 'films').returns(swapiFilmListMock);
    const response = await starwars.filmList();
    expect(response).to.deep.equal(starwarsFilmListMock);
  });
});
Enter fullscreen mode Exit fullscreen mode

Here, the describe block is defining an occurrence of the test.

You would normally use describe and wrap that with an it . This enables you to group tests so that describe can be thought of as a name for the group and it can be thought of as the individual tests that will be run.

You’ll also notice we have an afterEach function.

There are several of these type of functions that work with Mocha.

Typically, the one’s you’ll see most often are afterEach and beforeEach. These are basically lifecycle hooks that enable you to setup data for a test, and then free resources after a test is run.

There’s a swapi.films.restore() call within the afterEach.

This frees up the SWAPI films endpoint for stubbing and future tests. This is necessary since the starwars controller that I’m testing is calling the SWAPI films endpoint.

In the it block, you’ll notice there is a definition followed by an async function call. The async call here indicates to the runner that there is asynchronous behavior to be tested. This enables us to use the await call that you see in line 7.

Finally, we get to the test itself.

First, we define a stub with:

sinon.stub(swapi, 'films').returns(swapiFilmListMock);
Enter fullscreen mode Exit fullscreen mode

This stub signals to Mocha to use the mock file whenever the films method is called from the swapis API service.

To free up this method in your test runner, you’ll need to call the restore.

This isn’t really a problem for us here since we’re just running one test, but if you had many tests defined then you’d want to do this. I’ve included it here just to indicate convention.

Finally, we have our actual method call and an expect to check the result:

const response = await starwars.filmList();
expect(response).to.deep.equal(starwarsFilmListMock);
Enter fullscreen mode Exit fullscreen mode

When you run this test, it should call the filmList controller, and return what would be expected with the starwarsFilmListMock response.

Let’s run it.

Install Mocha globally in your terminal with:

npm i mocha --global
Enter fullscreen mode Exit fullscreen mode

Then, run the test with:

mocha test/firstUnit
Enter fullscreen mode Exit fullscreen mode

You should see the following:

ms-starwars

On a high level, this is what you can expect with any unit tests.

Notice that we did the following:

  1. Arrange – we set up our data by creating a stub
  2. Act – we made a call to our controller method to act on the test
  3. Assert – we asserted that the response from the controller equals our saved mock value

This pattern of Arrange, Act, and Assert is a good thing to keep in mind when running any test.

A more complicated unit test

This first test showed you the basic set-up — you now have a basic understanding of arrange, act, and assert.

Let’s consider a more complicated test:

describe('Film', function() {
  afterEach(function() {
    swapi.film.restore();
    swapi.people.restore();
  });
  it('should return all the metadata for a film when called', async function() {
    const filmId = '1';
    const peopleId = '1';
    const planetId = '1';
    const starshipId = '2';
    const vehicleId = '4';
    const speciesId = '1';
    sinon
      .stub(swapi, 'film')
      .withArgs(filmId)
      .resolves(swapiFilmMock);
    sinon
      .stub(swapi, 'people')
      .withArgs(peopleId)
      .resolves(swapiPeopleMock);
    sinon
      .stub(swapi, 'planet')
      .withArgs(planetId)
      .resolves(swapiPlanetMock);
    sinon
      .stub(swapi, 'starship')
      .withArgs(starshipId)
      .resolves(swapiStarshipMock);
    sinon
      .stub(swapi, 'vehicle')
      .withArgs(vehicleId)
      .resolves(swapiVehicleMock);
    sinon
      .stub(swapi, 'species')
      .withArgs(speciesId)
      .resolves(swapiSpeciesMock);
    const response = await starwars.film(filmId);
    expect(response).to.deep.equal(starwarsFilmMock);
  });
});
Enter fullscreen mode Exit fullscreen mode

Wow, that’s a lot of stubs! But it’s not as scary as it looks — this test basically does the same thing as our previous example.

I wanted to highlight this test because it uses multiple stubs (with args).

As I mentioned before, ms-starwars bundles several HTTP calls under the hood. The one call to the film endpoint actually makes calls to film, people, planet, starship, vehicle, and species. All of these mocks are necessary to do this.

Generally speaking, this is what your unit tests will look like. You can do similar behaviors for PUT, POST, and DELETE method calls.

The key is to be testing code. Notice that we made use of a stub and mock in our return value.

We were testing the application logic, and not concerned with the application working in its entirety. Tests that test full flows are typically integration or end-to-end tests.

Integration tests

For the unit tests, we were just focused on testing the code itself without being concerned with end-to-end flows.

We were only focused on making sure the application methods have the expected outputs from the expected input.

With integration tests (and also for end-to-end tests), we’re testing flows.

Integration tests are important because they make sure individual components of your application are able to work together.

This is important with microservices because you’ll have different classes defined that (together) create a microservice.

You might also have a single project with multiple services, and you’d write integration tests to make sure they work well together.

For the ms-starwars project, we’re just going to make sure that the orchestration provided by the controllers work with the individual API calls to the SWAPI endpoints.

Go ahead and define a new file with /test/firstIntegration.js.

Add the following to the top of the file:

const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const app = require('../server');
const should = chai.should();
const expect = chai.expect;
// starwars mocks
const starwarsFilmListMock = require('../mocks/starwars/film_list.json');
Enter fullscreen mode Exit fullscreen mode

What is this doing?

First, we’re defining an instance of chai and chai-http. Next, we’re defining an instance of the actual app itself from the server.js file.

Then we’re pulling in should and expect, and finally we’re pulling in a mock that we’re going to use to compare the response.

Let’s build our test:

describe('GET /films-list', () => {
  it('should return a list of films when called', done => {
    chai
      .request(app)
      .get('/films-list')
      .end((err, res) => {
        res.should.have.status(200);
        expect(res.body).to.deep.equal(starwarsFilmListMock);
        done();
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

So what’s this doing?

Well, this is similar to the syntax we saw before — we have the describe with an it. This sets up the test and indicates that the test is actually occurring here.

Then we make a call to chai.request and pass our reference to our app (server.js) file. This is how we can engage the chai-http library to make our HTTP call.

We then are passing a GET call to the films-list endpoint from our API.

Then we call end to signal behavior on what to do when the call completes.

We expect a status of 200 with:

res.should.have.status(200);
Enter fullscreen mode Exit fullscreen mode

Then we expect a body to equal our mock with:

expect(res.body).to.deep.equal(starwarsFilmListMock);
Enter fullscreen mode Exit fullscreen mode

Finally, we call done() to stop the test runner.

The really cool part about this is that it starts your application locally, runs the request you specify (GET, POST PUT DELETE, etc.), enables you to capture the response, and brings down the local running application.

So now with our integration test set up, run it with the following:

    mocha --exit test/firstIntegration
> note that the `--exit` flag is being passed here just to signal to the test runner to stop after the test finishes.  You can run it without `--exit` , but it would just wait for you to manually cancel the process.
Enter fullscreen mode Exit fullscreen mode

Then you should see something like this:

git-master-nocdn.png

There are other frameworks that can literally run your application beside your test runner.

However, using chai-http is clean and easy to implement with any of your projects and doesn’t require additional frameworks in general.

I recommend playing with the chai-http library and your application, and consulting the documentation when you have questions.

Testing strategies

With any test suite, we should also consider an overall strategy. You should ask yourself, what do you want to test? Have you covered all the application flows? Are there specific edge conditions that you want to test? Do you need to provide reports for your Product Owner or Team Lead?

The frameworks I’ve covered so far enable you to run tests, but there are many options for test reporters. Additionally, there are several test tools out there that provide code coverage.

If you consult the post here, you’ll see a good example of the Istanbul Test Coverage Tool.

One of the failures that I’ve experienced with teams is that they think if the code coverage tool says you’ve got 90% coverage, then you’re good. This isn’t really accurate.

When you write your tests, you should consider odd behavior and testing for specific inputs. Just because your code has been covered doesn’t mean that the outliers and edge cases have been covered.

With any test suite, you should consider not just the “happy path” and “sad path” scenarios, but also edge cases and specific cases to your customers.

Additionally, there are often times with integration and end-to-end tests that you may be reliant on external HTTP calls.

This could be problematic if the external APIs are down.

I actually recently built another microservice that did just that. I employed a mock server to run my tests, and used start-server-and-test to run both together.

This proved to be a great experience because I could run my tests in isolation, and it freed me up from relying on the external APIs.

I recommend checking out my article here. This is a great example of an innovative approach to testing without dependencies.

Overall, your test strategy is going to be based on your situation. I recommend you look beyond just “happy path” or “expected cases” and consider everything else.

Conclusion

I hope my post here has given you a good introduction to testing your Node.js applications.

We’ve discussed the different frameworks and technologies you can use in your Node.js applications. We’ve also walked through unit and integration tests for your Node.js applications.

The framework I used here was Express.js, but these patterns could apply to other Node.js frameworks as well. I recommend checking out the links I’ve provided above, as well as the documentation for each framework.

Follow me on twitter at @AndrewEvans0102.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Unit and integration testing for Node.js apps appeared first on LogRocket Blog.

Top comments (1)

Collapse
 
kaushalgautam profile image
Kaushal Gautam

Hey Brian! Thank you so much for this article. Testing has always been on of the dark areas of development for me and I struggle to see the 'how' of tests. I really did not understand how anything apart from Unit tests was possible. However, with this article, I am a step closer to seeing eye to eye with tests. Thanks again! :)