How To Test a Node.js Module with JavaScript using Mocha and Assert

Kayode - Mar 11 '22 - - Dev Community

Software testing is an integral part of software development and quality assurance. Testing can help us write better and quality code, spot ambiguities early, catch regression, refactor with confidence and also deploy to production while enjoying a cup of coffee .

https://media.giphy.com/media/dGhlifOCTtSdW/giphy.gif

We need to have a structured test with test cases that verify different behaviours of our application. Mocha is a JavaScript framework that organizes our test cases and runs them for us. Although Mocha will not verify our tests behaviours, we are going to make use of Nodejs assert library to verify our test behaviours.

Introduction to NodeJS Module

NodeJS is a runtime environment built on Chrome’s V8 engine that enables us to write and run JavaScript outside a web browser. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

NodeJS modules are blocks of code with specified functionalities that can be used with an external application based on their respective functionalities. The core idea of NodeJS Modules is encapsulation, reusability and modularity. Modules can be classified into three parts such as:

  • built-in modules: are modules that are part of NodeJS. They are readily available in our NodeJS installation and we can make use of them in our application by calling the require function with the name of the module as an argument. For instance:
const path = require('path')
Enter fullscreen mode Exit fullscreen mode
  • local modules: these are modules that we can create locally in our application and can be reused in our application.
  • third-parties modules: these are modules that are provided from external sources other than our local application and NodeJS Core Modules. Mocha is a third-party module that we have to install from external sources.

Prerequisites

  • NodeJS: which can download here. We can verify if NodeJS is installed in our machine by running this command ( node --version ) in our terminal. We should get the installed version in our terminal such as:

node-version.PNG

  • Any suitable code editor of our choice. Although I will be making use of Visual Studio Code which can be downloaded here.

Sample Use Case of a Core NodeJS Module

We will write a simple NodeJS application called core_modules.js that makes use of the built-in module path to print the extension type of our program which is .js

const path = require("path")

const extension = path.extname(process.argv[1])

console.log(extension)
Enter fullscreen mode Exit fullscreen mode

The path.extname functions take a string argument (path) and return the extension of the file in the path. When we run this code by running the command, node core_modules.js, in our terminal. process.argv is an array and the second element (index 1) in it is the path to our running JavaScript file.

Running the code above, we should get the result: .js.

Writing a NodeJS Module

Now we are going to write a NodeJS module that mocks a student managements application. This module would be able to store a list of students, add new students to the list, get the list of students and grade student performance between the range of 0 to 100.

Having the prerequisites above in place, we will create a new folder and initialize our application environment. In our terminal, we will create a new folder by running the command

$ mkdir students-manager
Enter fullscreen mode Exit fullscreen mode

Change our terminal’s current working directory into the students-manager folder by running

$ cd students-manager
Enter fullscreen mode Exit fullscreen mode

Next, we will initialize npm, which is needed because we are going to install Mocha via npm.

$ npm init -y
Enter fullscreen mode Exit fullscreen mode

The -y option allows npm to start our project using the default options:

We will create an index.js file where we can start writing our NodeJS module.

// index.js
let counter = 0;

const generateId = () => {
  counter++;
  return counter;
};

class StudentManagement {
    #students = []

  constructor(students) {
    this.#students = students.map((student) => {
      return { id: generateId(), name: student, performance: null };
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, we created a class with a constructor that takes an array of strings (student names) as an argument and converts each array item to an object with an id, name and performance properties. The #students is a private property of the class that can only be accessed internally.

The generateId is a helper function that increments the counter variable by one and returns the current value of the counter. The returned value will be used for generating a unique id for each student created.

The generateId and counter represent a feature of modules which is, encapsulation. Encapsulation helps us hide implementation and expose functionality. A real-world scenario is how vehicles work, many of us do not really know how the engines work and the gear system works, we are exposed to the functionality of the car which is majorly driving.

Let’s create a method, called add(), to add a student to the list of student:

// index.js
let counter = 0

const generateId = () => {/* */}

class StudentManagement {
  constructor(students) {/* */}

  add(student) {
    this.#students.push({
      id: generateId(),
      name: student,
      performance: null,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The add() method takes a string (student name) and creates an object the string as a name property and the student performance set to null

What if we want to add a batch of student names, it would make sense to be able to add in a single name or multiple arguments of name using the same function so we will rewrite the add() method.

// index.js
let counter = 0;

const generateId = () => {/* */};

class StudentManagement {
  constructor() {}

  add(...students) {
    students.forEach((student) => {
      this.#students.push({
        id: generateId(),
        name: student,
        performance: null,
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, we will include a method, called getStudent() that returns an array of the student(s) and their details.

// index.js
let counter = 0;

const generateId = () => {/* */};

class StudentManagement {
  constructor() {/**/}

  add() {/**/}

    getStudent(studentIdOrName) {
    if (studentIDOrName) {
      return this.#students.filter(
        (student) =>
          studentIDOrName === student.id || studentIDOrName === student.name
      );
    }
    return this.#students;
  }
}
Enter fullscreen mode Exit fullscreen mode

The getStudent() method works in two ways depending on if studentIdOrName is passed as a parameter. studentIdOrName can be an id of a student (number) or the name of a student (string).

The method returns just a single student if the id or name of the student is found, else it just returns the whole students list.

We are making use of the filter() which is an array method. The filter() loops through each item in an array and make that item accessible via the callback function we passed to it. If the callback returns true, the filter() includes that item in its result.

Let’s create a method to be able to grade the students, the function would take two arguments, one for student id and the other for the score of the student which should be a number between 0 and 100.

// index.js
let counter = 0;

const generateId = () => {/* */};

class StudentManagement {
  constructor() {/* */}

  add() {/* */}

  getStudent() {/* */}

  score(id, performance) {
    if (performance >= 0 && performance <= 100) {
      this.#students.find((student) => {
        if (student.id === id) {
          student.performance = performance;
        }
      });
    } else throw new Error("Score should be between 0 and 100");
  }
}
Enter fullscreen mode Exit fullscreen mode

Our score() method checks if the performance is between the range of 0 - 100 if the argument is lesser than 0 or greater than 100, we will raise an Error. The method checks through the list of students and finds the student with a similar id provided in the method's first argument.

Up until now, the index.js file is not a module yet so we need to export it to make it reusable across our local application and test it.

// index.js
let counter = 0

const generateId = () => { /* */}

class StudentManagement { /* */ }

module.exports = StudentManagement
Enter fullscreen mode Exit fullscreen mode

NodeJS uses the CommonJS convention for modules, hence we now have our module exported and ready to be used across our local application.

Once done, our index.js should be similar to this:

// index.js
let counter = 0;

const generateId = () => {
  ++counter;
  return counter;
};

class StudentManagement {
  #students = [];

  constructor(students) {
    this.#students = students.map((student) => {
      return { id: generateId(), name: student, performance: null };
    });
  }

  add(...students) {
    students.forEach((student) => {
      this.#students.push({
        id: generateId(),
        name: student,
        performance: null,
      });
    });
  }

  getStudent(studentIDOrName) {
    if (studentIDOrName) {
      return this.#students.filter(
        (student) =>
          studentIDOrName === student.id || studentIDOrName === student.name
      );
    }
    return this.#students;
  }

  score(id, performance) {
    if (performance >= 0 && performance <= 100) {
      this.#students.find((student) => {
        if (student.id === id) {
          student.performance = performance;
        }
      });
    } else throw new Error("Score should be between 0 and 100");
  }
}

module.exports = StudentManagement;
Enter fullscreen mode Exit fullscreen mode

Manually testing our code to see how it functions

Next, we will manually make use of our project and see how it works.

We create a demo.js file in the root directory of our students-manager folder and make use of the module by calling the require function.

// demo.js
const StudentManagement = require("./index")

const classA = new StudentManagement(["Abel", "Ben", "Cain"])

console.log("Intial Students: \n", classA.getStudent())

// adding two new students
classA.add("Daniel", "Evelyn")

// scoring Abel and Ben
classA.score(1, 50)
classA.score(2, 49)

// print the students list using the print() method 
console.log("\n\n")
console.log(classA.getStudent())
Enter fullscreen mode Exit fullscreen mode

When we run this application by calling node demo.js we can see the result verify that the module works as expected.

Here’s a screenshot of the result :

demo-result.PNG

Writing our now test our module with Mocha and Assert

Remember when we spoke about the NodeJS module, we spoke about the different types of modules, assert is a built-in module while mocha is an external module so we have to install it into our project using npm.

In our terminal, ensuring that we are still in the students-manager directory, we will install mocha by running the command:

npm install mocha --save-dev
Enter fullscreen mode Exit fullscreen mode

The --save-dev flag saves mocha as a development dependency in our NodeJS because we only want to write tests in the developments stages and not include them in production deployments.

Let us create our test file, called index.test.js and include our local StudentManager module and also the asset module in the current working directory:

// index.test.js
const assert = require("assert")
const StudentManagement = require("./index")
Enter fullscreen mode Exit fullscreen mode

Mocha helps us organize and run our tests. The test structure is usually structured as below

describe("The Test Group", () => {
  it("the title of the test", () => {
    // the test code is here
  });
});
Enter fullscreen mode Exit fullscreen mode

The it function contains our test code. This is where we make use of our assert module to test our StudentManagement module.

The describe function is not necessary for Mocha to run our test but it helps group our tests and easily manage them.

Let’s define our test cases and write the implementation of these tests cases.

Test Cases

  • initialize our module with three names: Abel, Ben and Cain
  • confirm that the module has a total of three students
  • add two new students and confirm that number of students increased to five
  • score Abel with 50 and confirm that the score is included in Abel performance metric
  • score Ben with 120 and confirm that the module throws an error

Test Implementation

  • We will create our test file in the root directory of our folder: index.test.js
// index.test.js
const assert = require("assert");
const StudentManagement = require("./index");

describe("Student Management Module", () => {
  let testClass;

  before(() => {
    testClass = new StudentManagement(["Abel", "Ben", "Cain"]);
  });

  it("should have three students", () => {
    assert.equal(testClass.getStudent().length, 3);
  });

  it("adds new students and confirm that the new students are added", () => {
    testClass.add("Daniel", "Evelyn");
    assert.equal(testClass.getStudent().length, 5);
  });

  it("checks the content of the students list and verify it", () => {
    const expectedStudentList = [
      { id: 1, name: "Abel", performance: null },
      { id: 2, name: "Ben", performance: null },
      { id: 3, name: "Cain", performance: null },
      { id: 4, name: "Daniel", performance: null },
      { id: 5, name: "Evelyn", performance: null },
    ];

    assert.deepEqual(testClass.getStudent(), expectedStudentList);
  });

  it("score Abel and confirm that Abel is scored", () => {
    testClass.score(1, 50);
    const abelStudentObject = [{ id: 1, name: "Abel", performance: 50 }]

    assert.deepEqual(testClass.getStudent(1), abelStudentObject)
    assert.deepEqual(testClass.getStudent("Abel"), abelStudentObject)
  });

  it("should verity there is an error is score is greater than 100", () => {
    assert.throws(() => {
      testClass.score(1, 105);
    }, Error)
  })
});
Enter fullscreen mode Exit fullscreen mode

Code Walkthrough

  1. Import both the custom module we created and the assert module for verifying the test behaviour

    const assert = require("assert");
    const StudentManagement = require("./index");
    
  2. describe(): this function as we mentioned earlier is used for grouping our tests and adding a description to the test. Since we added our test to this block, the before() method is a hook that runs before the first test is started. A testClass is defined in this block to make it globally available to all our tests.

  3. In the before function, a new instance of the StudentManagement is created with three students and passed to the testClass variable

    before(() => {
        testClass = new StudentManagement(["Abel", "Ben", "Cain"]);
    });
    
  4. assert.equal() checks if two variables are equal, it uses the == operator. This type of equality checks tries to covert the variables of different types to the same times unlike assert.strictEqual(). The assert.strictEqual() makes use of the === operator.

  5. assert.deepEqual() is used to check for the equality of the objects, which does a comparison of the enumerable properties of an object

  6. To assert if an error occurred in an application, the assert.throws() methods would only pass if an error is thrown it the callback passed to it.

    assert.throws(() => {
        testClass.score(1, 105);
    }, Error)
    

    The second argument, Error is a constructor for creating errors. For instance:

    const OutOfBoundError = new Error("The index is out of bounds")
    

    The assert.throws() verifies if the type of error thrown in the callback is of the same type with the Error object passed in the second argument.

Running out Test

To run our test, we are going to make use of Mocha by running the below command in our terminal in the root of our current working directory which is students-manager:

npx mocha index.test.js
Enter fullscreen mode Exit fullscreen mode

But npm has a feature where we can define different sets of commands and make them simple and shared. The commands are found in the scripts section of our package.json file.

When we ran npm init -y, npm creates a package.json file for us and when we installed Mocha, npm updated this file for use with the installed dependencies.

Let’s create our script called:

//.
  "scripts": {
    "test": "mocha index.test.js"
  },
//..
Enter fullscreen mode Exit fullscreen mode

Then our final package.json should be similar to this:

{
  "name": "mocha_assert",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "mocha index.test.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s start our test by running in our terminal:

npm test
Enter fullscreen mode Exit fullscreen mode

The output of our test in the terminal:

test-result.PNG

It’s a Wrap

In this tutorial, we got introduced to NodeJS modules, how to require modules in our application and how to create our module. We created a NodeJS that mocks a student management application and wrote tests based on the functionalities exposed by the module with Mocha and assert for verifying our test and enjoyed a cup of coffee doing this

https://media.giphy.com/media/1qh9vOiVTo7tpei8BZ/giphy.gif

Mocha is a feature-rich JavaScript test framework running on NodeJS and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting while mapping uncaught exceptions to the correct test cases.

Challenge yourself by writing tests for your subsequent NodeJS modules.

To deep dive into Mocha, you can check out the official Mocha documentation. And to continue learning about the NodeJS module, you can check the NodeJS documentation on modules

If you enjoy reading this article, you can consider buying me a coffee

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .