Node.js movie app - Write tests with Mocha, Chai and Sinon

Writing tests is an important but often less liked part of coding. In this article I will write some test cases to the application built in the few posts.

Part 1: Create the framework

Part 2: Make it interactive

Part 3: Add real data

Part 4: Complete the application

The tests to be written are related to the movie app we created in the last four posts (see links above). You can read them and build the app step-by-step or download the code from Github.

Install the dependencies

We will use the Mocha testing framework and the Chai assertion library to write the test cases.

I wrote two posts earlier on how to install them and why they are useful, so if you have any doubts or are not familiar with the stack, read these posts first.

Set up the environment. Let’s start by installing Mocha and Chai as dev dependencies. Navigate to the project folder and enter npm install --save-dev mocha chai.

When done go to package.json and set up the command which will run the tests. In the scripts object, we can write the following:

"scripts": {
  "test": "mocha modules/**/*.test.js"
}

This basically means that every time we type the npm run test command, Mocha will find all files ending in .test.js in the modules folder and its subfolders and run the test cases inside those files.

Create the test file. We will only test the functions in user-input.js file. This file contains the two prompts and their validator functions.

Create a file called user-input.test.js in the modules folder, so that the naming of the test file matches the one containing the logic which we want to test.

Add tests

We won’t go crazy here and will only write a few test cases. Feel free to extend the list of tests if you wish.

Test the validator functions

We’ll start by writing unit tests to the prompt validator functions.

First, let’s import Chai and the validator functions at the top of user-inputs.test.js:

const chai = require('chai');

const {
  createMovieTitleList,
  isTitleEntered
} = require('./user-inputs');

const { expect } = chai;

Create the first test cases. Declare the name of the module we want to test in the describe block and we are now ready to write tests to the isTitleEntered validator function:

describe('#user-inputs', () => {

  describe('#isTitleEntered validations', () => {
    it('should return true if a string is entered', () => {
      const userInput = 'Taken';

      const validationResult = isTitleEntered(userInput);

      expect(validationResult).to.be.true;
    });

    it('returns a warning if nothing is entered', () => {
      const userInput = '';

      const validationResult = isTitleEntered(userInput);

      expect(validationResult).to.be.equal('Please enter a title!');
    });
  });

});

As you can see I placed these two test cases inside another describe block to separate them from the rest of the tests. This is of course not necessary and it’s only a personal preference. I like the test cases to be well separated.

Arrange, Act, Assert. It’s a good idea to follow the Arrange, Act, Assert principle laid down in Clean Code as much as possible and I strive to write my tests this way whenever possible.

There are two test cases here; the first one tests if isTitleEntered returns true on a valid entry (i.e. a string), while the second test asserts if the validator returns the custom error message if the user didn’t type in anything.

In both cases we defined our input (Arrange) first and saved it into the userInput variable. Then we triggered the isTitleEntered function with that input (Act) and finally we checked if the result we got (validationResult) is the same as what is expected based on the function’s returned value.

If you run npm run test from the project root, you should see the tests pass.

Test createMovieTitleList()

Let’s write now a test case to createMovieTitleList. This function accepts an array of movie objects as an argument and returns another array of objects with the title, the year and the id of the movie.

Inside the main describe block we can create another one for this function and we can write the following test:

describe('#createMovieTitleList', () => {
  it('returns name and value properties', () => {
    const responseFromDB = [
      {
        Title: 'Taken',
        Year: '2008',
        imdbID: 'tt0936501',
        Type: 'movie',
      },
      {
        Title: 'Taken 2',
        Year: '2012',
        imdbID: 'tt1397280',
        Type: 'movie',
      }
    ];
    const expectedNameAndValue = [
      {
        name: 'Taken (2008)',
        value: 'tt0936501'
      },
      {
        name: 'Taken 2 (2012)',
        value: 'tt1397280'
      }
    ];

    const nameAndValue = createMovieTitleList(responseFromDB);

    expect(nameAndValue).to.be.deep.equal(expectedNameAndValue);
  });
});

We simulate the value of Search property (returned from the database), which is the input of createMovieTitleList. This is what the responseFromDB array represents. I only display the most necessary properties in the objects to try to keep the code at the minimum necessary level.

The expectedNameAndValue array as the name suggests is what createMovieTitleList should return; the array of choices will look like similar to this. The elements of the array will be displayed in the CLI prompt as the list of choices.

We assert that the two arrays should be equal, and indeed, this test will also pass.

Test the prompts

Writing tests to the prompts is somewhat trickier because the prompts return a Promise.

Chai as Promised. We’ll install an extension to Chai called Chai as Promised, which makes it possible to assert promises as well. So let’s do so quickly by typing npm install --save-dev chai-as-promised in the terminal.

In order to use the methods of the extension we need to make a small change in the top of the user-inputs.test.js file:

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');

const { expect } = chai.use(chaiAsPromised);

We can now use the methods of both chai and chai-as-promised.

The first test. We’ll write two test cases, one for each return path. If the movie doesn’t exist in the database, an error message is returned inside a rejected promise, otherwise we’ll get the response object with the details of the movies.

In both cases we test the getSelectedMovieFromUser, so we’ll need to require it:

const {
  createMovieTitleList,
  getSelectedMovieFromUser,
  isTitleEntered
} = require('./user-inputs');

Let’s have a look at the first case first.

In this case getSelectedMovieFromUser returns a rejected promise with a custom error message. Inside the main describe block we can create another one for this function as well and the first test case can look like this:

describe('the prompt', () => {
  it('returns error message if movie does not exist in the database', async () => {
    const responseFromDB = { Error: 'Error message from DB' };

    const responseFunc = () => getSelectedMovieFromUser(responseFromDB);

    await expect(responseFunc()).to.be.rejectedWith('The requested movie is not found.');
  });
});

We use the rejectedWith method of chai-as-promised here. To test promises expect should be fed with a function (responseFunc) which returns the function we want to test (getSelectedMovieFromUser). This is the function that returns the promise. Yeah, this might sound confusing for the first time, I know, but this is how Chai works.

As it can be seen, we expect that getSelectedMovieFromUser be rejected with the custom error message.

Let’s run the test command again and all tests should pass again. To check if we really test how the function works, change a letter in the error message and the test case should fail.

Use Sinon

We’ll test one more case and this is when the prompt returns the correct response with the movie details.

Stub Inquirer. We know that getSelectedMovieFromUser calls Inquirer, which returns a Promise resolved with an object that has a movieId property with the id of the selected movie.

We don’t want to call Inquirer in the test because we don’t want to run the CLI. We are only interested in the returned promise and that Inquirer has been called.

In these cases we can use a stub, which takes over the role of Inquirer.

Sinon is a great library which provides spies, stubs and mocks for many testing frameworks. Let’s install it with npm install --save-dev sinon and then require both Sinon and inquirer in the test file:

const sinon = require('sinon');
const inquirer = require('inquirer');

The test case. We can write something like this in the describe block of getSelectedMovieFromUser:

it('should return a resolved promise if the movie exists', async () => {
  const inquirerStub = sinon.stub(inquirer, 'prompt');
  inquirerStub.resolves({ movieId: 'tt3896198' });
  const responseFromDB = {
    Response: 'True',
    Search: []
  };

  const responseFunc = () => getSelectedMovieFromUser(responseFromDB);

  await expect(responseFunc()).to.be.fulfilled;
  inquirerStub.restore();
});

Let’s go over the code step-by-step.

First we create the stub using the stub method of sinon. We provide two arguments: The first one is the module we want to substitute (inquirer) and the second one is the method itself (prompt).

We know that inquirer returns a resolved promise if the input is valid (i.e. the user entered a valid title), so we resolve the stub in the second step with the resolves method.

Next we create a fake response from the database (responseFromDB) but we are not interested in the response itself but in the fact that there is a response. So we only provide the minimum required properties for the response object without any real data.

We call responseFunc, which returns getSelectedMovieFromUser and assert that it’s fulfilled. The async/await syntax is used here as we work with a promise.

Finally we restore the stub with the restore method and set inquirer back to what it was before it got stubbed.

Done! Let’s run the npm run test for the last time and all test cases should pass.

Conclusion

This concludes the series of the Node.js CLI tutorial. The movie app is complete and works well.

The app can be enhanced and new features can be added upon personal preference. Feel free to download the code from Github and change it as you like.

Thanks for reading and see you next time.