How to create a Node.js app with Docker, Part 4: Testing

In the fourth part of this series we'll see how to create easily a complete testing environment for our Node.js application with Docker, Mocha and Chai.

In the previous posts, we created a Node.js web application and we learned how to manage its deployment, development and debugging with Docker. Although in the early stages of development it seems that manual testing is enough to check that our app works properly, the growth of its codebase and complexity will make this solution very inefficient and we will need automatic, fast and widespread tests to check the behavior of app's components.

It's easy to create a testing environment and it doesn't need particular configurations as regards the Docker container. Meanwhile, on the app side, we can use the stack that we want. In this guide, we'll use Mocha as test framework and Chai as assertion library.

Prerequisites

  • The Dockerfile, index.js and package.json files from the previous post.

Install Mocha and Chai

Before building a new image, we have to update the package.json file and add mocha, chai and chai-http (a Chai plugin for testing HTTP endpoints) to the dependencies. We can do it in several ways:

  • Adding the dependencies to the file manually.
  • Starting a container, update the file inside the container and propagate changes to the outside by using a volume (we saw how to do in the second article of this series).
  • Starting a container, update the file inside the container and copy it to the outside in our working directory.

Since the first option doesn't need further clarification and the second one has already been examined, let's take the opportunity to see a new approach.

If you haven't already created an image from the Dockerfile, do it now

$ docker build --tag my-app .

Run a container from the image and edit the entrypoint to launch a shell in it (we already discussed on this practice in the post about the debugging).

$ docker run -it --entrypoint /bin/sh my-app

Install the needed dependencies with NPM

# npm install -D mocha chai chai-http

When the installation it's over, exit from the shell (you can do with CTRL + D or exit command) and copy or overwrite the package.json file from inside the container to the outside, in our working directory.

$ docker cp <CONTAINER_ID>:app/package.json ./package.json

Write the tests

We'll create a test to verify that the application shows the 'Hello world' message on the homepage.

But first, we have to make a change in the index.js

var express = require('express')
var app = express()
var port = process.env.PORT || 3000

app.get('/', function (req, res) {
  res.send('Hello World!')
})

if (process.env.NODE_ENV === 'test') {
  module.exports = app
} else {
  app.listen(port)
}

We added an if block to check, by using the NODE_ENV environment variable, if the code is executed in a testing context and if so, the app object will be exported (we'll see why shortly) otherwise the application will be started normally.

Let's move on to the test. Since Mocha runs all tests inside the test folder by default, create in your working directory the following test/homepage.js file

// Import the dependencies
var chai = require('chai')
var chaiHttp = require('chai-http')
// Import the application to test
var app = require('../index')
// Configure Chai
chai.use(chaiHttp)
chai.should()

describe('Homepage', () => {
  it('should show the Hello World message', done => {
    chai
      .request(app)
      .get('/')
      .end((error, response) => {
        response.text.should.equal('Hello World!')
        done()
      })
  })
})

Let's see more in detail how this test is structured:

  • We use chai.use() to add the chai-http plugin to Chai.
  • We use chai.should() to choose should as assertion style. Other styles are assert and expect.
  • We use it() to create a test, describe() to group them in suites.
  • With request() we set an Express application of which we want test the endpoints with Chai.
  • In Mocha, we use done() at the end of a test to manage the assertions rightly in asynchronous functions like end().

Run the tests

Let's add a test script to the package.json file to launch Mocha (note that here we pass the NODE_ENV environment variable used in index.js)

{
  "name": "nodejs-app",
  "dependencies": {
    "express": "^4.17.0"
  },
  "scripts": {
    "start": "node index.js",
    "dev": "npx nodemon index.js",
    "debug": "node --inspect=0.0.0.0:9229 index.js",
    "test": "NODE_ENV=test npx mocha"
  },
  "devDependencies": {
    "chai": "^4.2.0",
    "chai-http": "^4.3.0",
    "mocha": "^6.1.4",
    "nodemon": "^1.19.1"
  }
}

Build a new image of our app and run a container

$ docker build --tag my-app .
$ docker run -d -p 4000:3000 my-app

At this point we can execute the tests in two ways:

  • we can run them like a process outside the container (this choice is made, for example, when we need to run them in a CI/CD pipeline)
$ docker exec <CONTAINER_ID> npm test
  • we can run them like a process inside the container by opening a shell in it
$ docker exec -it <CONTAINER_ID> /bin/sh
# npm test

Conclusions

Now we learned how to manage easily every phase of Node.js application development by using Docker. In the next post, we'll talk about services integration and we'll see how to include a database in our application by using a new tool, Docker Compose.