Node.js movie app - Write tests with Mocha, Chai and Sinon
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.