4.1.2: Backend Expressjs Testing

Learning Objectives

  1. Understand how to test Expressjs applications using supertest to preform end to end testing

  2. Understand how to test Expressjs controller functions using mocha and chai to ensure they are calling the correct database methods and responding correctly.

Setup

To showcase how to use Supertest as well as unit testing we will be utilising the ExpressJS Fruit Backend, auth0 is implemented to protect certain routes and we are getting data from a Sequelize database. Fork and clone Rocket's testing-express repo, please follow this setup to test out the repository. Inside this repository we have installed the required packages to run and test the application. The new testing libraries that we added include supertest, mocha, chai, sinnon and sinnon-chai. We use supertest and chai to preform tests on the Server. While we use mocha, chai, sinon and sinon-chai to test the Server controller methods.

Once you have cloned the repository onto your local machine install all of the dependencies with the command, run the command in the root of the project directory

npm install

You will then need to alter the sample.env and add in your own credentials.

After you have installed the relevant packages you will need to run some commands to develop a test database that your application will get information from. The test database will need to have a name that is not the same as any database currently running within your Postgres server. Run these commands in the root of the project directory

npx sequelize db:create
npx sequelize db:migrate
npx sequelize db:seed:all

Following this you will be able to run React tests by running the command below within the root of the project directory.

npm test

Supertest

Introduction

Supertest is a brilliant library that allows developers to test Node.js HTTP server as well as ExpressJs applications. I contains a simple and easy to use API for testing HTTP requests on your server, along with great assertions to ensure that responses are correct. Supertest is used with common testing libraries like Jest or Chai to supply assertions.

Lets look Supertest from a high-level overview:

  1. Create the request object:

    1. In order to make requests to your server through Supertest, we will first need to create a request object by utilising the request() method. As an argument method takes your ExpressJs server instance, it will then return a request object that you can then call HTTP requests on.

  2. Fire off HTTP request:

    1. Once the request object has been instantiated you are able to use invoke the various HTTP methods that you already know (get(), post(), put(), delete()...). These methods will be used to make requests to your server, the arguments for these methods are a URL path and any optional data as required.

  3. Assert the expected responses:

    1. After the request has been made you can use the expect() method to check the response and assert your expected outcomes. These methods allow you to check the response in the same way a browser checks, you can check virtually any property such as the status code, headers and body.

  4. Handle response errors:

    1. Supertest also allows you to handle any response errors that may occur then you make your requests. Such that you know your application is responding as expected under error conditions.

We are able to leverage Supertest along with Chai to test out our backend applications ExpressJs route handlers to ensure the application operates as expected.

Understanding Supertest

Supertest's flow is explained above, lets see how its implementend in code:

supertest.spec.js
const supertest = require("supertest");
const chai = require("chai");
const app = require("../index.js");
const expect = chai.expect;

// Get 
describe("GET / ", async () => {
  it("should recieve status code 200", async () => {
    const response = await supertest(app).get("/");
    expect(response.headers["content-type"]).equal("text/html; charset=utf-8");
    expect(response.status).equal(200);
    expect(response.text).equal("Incorrect path");
  });
});

In the example able we are using chai as our assertion/expectation library. On line 9 we creating our request object and firing off a get() request to the '/' url path. This response is stored in a variable and we must await the Supertest request to complete. Once its completed we are able to check the values within the response using our assertions from chai.

Checkout the example repository and see if you can apply this to your own code. If you would like to learn more about Supertest, checkout their documentation.

Unit Testing

Introduction

When testing your application you want to have as much test coverage as possible. While Supertest will test your controller functions by responding with the correct information from your linked database, you may want to ensure that your controller methods are always preforming as expected. You can do this by developing unit tests where by we mock the environment, request, response as well as database methods using Mocha and Chai. To test your backend controller mehtods you can follow these steps.

  1. Setup a test database:

    1. Before you run the tests it is important that you setup a test database such that your controller functions can access a operational database in the repository that we have supplied we are using sequelize as a database querier. This means we need to import the require model into the test while mocking the seqeulize package and methods.

  2. Decide on which controller functions to test:

    1. You should test the controller functions within your ExpressJs application that accesses your database to retrieve some information.

  3. Setup the files environment:

    1. We will use sinon to develop a sandbox environment for our tests. Then we will setup the controller within our beforeEach block. After which we define the information we will use to mock our database, the request objet as well as the response object. In the afterEach block we restore sinon and the sandbox.

  4. Writing test cases:

    1. For each test we need to create stubs that resolve data, we create stubs using sinon for each sequelize method we are testing. Then you can write test cases that simulate HTTP requests that your controller functions handle, by calling the controller method. We are then able to write assertions for the expected responses.

  5. Handle Response Errors:

    1. mocha and chai also allows you to handle any response errors that may occur then you make your requests. Such that you know your application is responding as expected under error conditions.

Understanding these tests

To understand how these tests operate and function please checkout the code and explanation below.

fruitControllerTest.spec.jss
// Get all imported packages and modules
const chai = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
const sequelize = require("sequelize");
const db = require("../db/models/index");

// Set up the fruit model, setup chai assertion
const { fruit } = db;
const expect = chai.expect;

// Tell chai to use sinonChai
chai.use(sinonChai);

// Start to write our testing environment
describe("fruitController", async () => {
  describe("List function", async () => {
    
    // Set up the sandbox using sinon so that we can keep tests seperate
    const sandbox = sinon.createSandbox();
    // Set up required variables
    let sampleReturnedFruitList, req, res, FruitController, fruitController;

    // before we run the test we will run this block
    beforeEach(() => {
      // define the fruit Controller that we will be testing
      FruitController = require("../controllers/FruitController");
      fruitController = new FruitController(fruit);
      // define the sample data we will run through the controller methods
      sampleReturnedFruitList = [
        {
          id: 1,
          name: "Apple",
          description: "This apple is crisp and sweet",
          colour: "Red",
          stock: 140,
          price: 15,
        },
        {
          id: 2,
          name: "Banana",
          description: "This banana is yellow and sweet",
          colour: "Yellow",
          stock: 200,
          price: 12,
        },
      ];
      // develop a mock request
      req = {};
      // develop a moct response
      mockResponse = () => {
        const res = {};
        res.status = sinon.stub().returns(res);
        res.json = sinon.stub().returns(res);
        return res;
      };
      res = mockResponse();
    });

    // This block runs after each test is run
    afterEach(() => {
      // restore all stubbed functions and the sand box
      sinon.restore();
      sandbox.restore();
    });
    
    // write out the what we are testing for
    it("Can calls the findAll method to get the data from the database", async () => {
      // Create a stub for the findAll method from sequelize, it resolves the sample data above
      let findAllStub = sandbox
        .stub(sequelize.Model, "findAll")
        .resolves(sampleReturnedFruitList);
      
      // call the controller mehtod
      await fruitController.list(req, res);
      
      // set up the assertions
      expect(findAllStub.calledOnce).to.be.true;
      expect(res.json.calledOnce).to.be.true;
      expect(res.status.calledOnce).to.be.false;
      expect(res.json).to.be.calledWith({
        fruit: sampleReturnedFruitList,
        message: "success",
      });
    });
  });

In the example above we have set up the testing environment and are testing the list function of the fruitController. We are checking to see if the method is being invoked, we then check is see if the sequelize interactions are being fired off, and then we can see if our code is executing correctly.

At the end of the day testing Express controller functions using Mocha and Chai will mean you need to set up a testing database, you need to have testable controller functions, write tests that simulate HTTP requests running through your server. Moreover you need to assert all responses and mock the external dependancies such that when you test your controller functions we can ensure that they are utilising the database and responding correctly.

Here are some helpful sets of documentation that should help you to extend these tests in your portfolio projects.

mocha: https://mochajs.org/

chai: https://www.chaijs.com/

sinon: https://sinonjs.org/

sinon-chai: https://www.chaijs.com/plugins/sinon-chai/

See if you can apply these types of tests into your applications. Checkout the package.json to see how we are running npm test.