Add Customer API – CustomerService – Create Customer Unit Tests – Node.js API with TDD Tutorial

So far, we have set up the module and test files with an initial set of test suites and functional code implemented successfully. Now it’s the time for developing the required functionalities as per the requirement specifications laid down in the earlier posts. 

CustomerService – Create Customer Unit Tests

In this section, we will start with writing test scripts and coding the required functional script for the CustomerService module file.

CustomerService Spec File Preparation

We will start by preparing the customer.service.spec.js file with the initial content needed for test suites.

1. Load Required Test Modules

Here is the first content for this test file where the list of test-related modules is necessary for the specs is loaded.

'use strict';

var chai = require('chai');
var expect = chai.expect;
var sinon = require('sinon');

var mongoose = require('mongoose');

You are already aware of the first two modules that are loaded here as we have used them in the previous modules. The last one, Sinon, is new here. We will be using Sinon for creating spies, stubs and mocks while testing the unit of code. Let’s look at what are these and when to use them.

What is a Test Spy?

A test spy is a function which can record and tracks its arguments, return value, the value this and an exception is thrown out of it for all of its calls. So we can check and confirm this information within the spec for any code which is being tested. 

We can create two types of spies: as an anonymous function and by wrapping method of an already existing system under the test. 

When we don’t need to test the behavior of the function under test, we can use the spy as an anonymous function. Here the spy will not do anything else other than recording the information on the call. 

We can use spy with wrapping the existing method or function while testing its behavior. In this context, the spy will exactly behave like the original method of function and also gather the information related to the call. So that we can make use of those call details within our spec. Once the test is completed, the spied-on method can be reverted to its original behavior by calling the spy.restore() function.

What is Test Stub?

A test stub is a spy function with pre-programmed behavior. So it behaves like a spy with all of the spy’s API available in addition to methods which can be used to modify the stub’s behavior. 

Just like the spies, the stub can be created as an anonymous function or by wrapping the existing functions. One more difference between the spy and stub is, while using the stub with an existing function wrapped, the original function is not called. So it’s our responsibility to add the required behavior to the stub as per the test.

Below are the situations where we can use the stub rather than the spy.

  1. We want to test a particular execution path of the method by controlling its behavior. For testing the error handling, we could force the method to throw an error by stubbing it.
  2. We don’t want to call a specific method to avoid an undesired behavior triggered. For testing the database or file system access method, we can stub the method with the expected behavior without actually accessing the database or the file system.

What is Test Mock?

A test mock is a fake method of pre-programmed behavior and pre-programmed expectations. So the mock will behave like a spy and stub along with pre-programmed expectations.

The mock should be used for the method that’s being tested in a unit test. Use a mock, if we want to control how the method, which is under test, is being used and like defining the expectations before-hand instead of asserting it later.

You need to install the Sinon package within this application before its use. Run the below command in the terminal from the application’s root directory to install and add it as a development dependency in the package.json file.

npm install sinon --save-dev

2. Load Component Files

Next, let’s load the module file which is being tested in this test suite. You can get the service and model attributes from the CustomerModule as below. CustomerService is the unit that you will be testing in this file and CustomerModel is the one that will be mocked.

var CustomerModule = require('../../../modules/customer/customer.module')();
var CustomerModel = CustomerModule.CustomerModel;
var CustomerService = CustomerModule.CustomerService;

3. Load Fixtures

Also, fixtures need to be loaded to use as input and write the expectation for the unit under test. Below lines will load the fixtures for both the customer and error. Although these fixtures are empty for now, you will be filling them with appropriate content in just a while.

var Fixtures = require('../../fixtures/fixtures');
var CustomerFixture = Fixtures.CustomerFixture;
var ErrorFixture = Fixtures.ErrorFixture;

Since the CustomerModel will be mocked with Sinon mock API, let’s create a variable as well.

var CustomerModelMock;

4. Prepare Main Test Suite

Next, let’s add the below main test suite in the file and more test suites and specs will be inserted inside of this one in the coming sections.

describe('CustomerService', function () {

});

Inside this test suite, the specs for testing the createUser function will be added to both success and failure cases. Since you will be running multiple tests for the createCustomer() function, let’s use the mocking strategy for the CustomerModel when you use it inside the spec. This way you can control the execution of both success and failure paths to simulate the all possible flows of the code.

You should create the mock before the test and destroy it after the test. This preconditions setup and cleanup can be accomplished with the hooks that Mocha provides before(), after(), beforeEach(), and afterEach(). For these tests, the mock needs to be created and destroyed for each test. So you can use the beforeEach() and afterEach() hooks.

So, add the below code blocks inside the main test suite CustomerService as this will be used for all other test suites that you will be adding in the coming sections as well.

beforeEach(function () {
    CustomerModelMock = sinon.mock(CustomerModel);
});

The function passed to the beforeEach() will be executed before each test runs. So each time, new CustomerModeMock is created with the use of the Sinon mock API. It mocks the CustomerModel which is used to create and read the Customer document in the MongoDB collection.

afterEach(function () {
    CustomerModelMock.restore();

    mongoose.models = {};
    mongoose.modelSchemas = {};

    return mongoose.connection.close();
});

Once a test’s execution is completed, the function in the afterEach() will be called. Thus, the mocked CustomerModel function will be restored so that all the behaviors of the actual CustomerModel will be back to its original format and it will be ready for the next test run. The restore() function in the mock is invoked to make it happen. 

Also, the next two statements will clear the mongoose models and schemas that are compiled or created during the test run. Finally, the MongoDB connection established via the mongoose module is closed.

Unit Test 11: CreateCustomer Success Test Case

Let’s start with the first test suite for the customer.service.js file. 

Test Suite

Add a new test suite inside the main suite of the customer.service.spec.js file.

describe('createCustomer', function () {
    
});

You would need a few variables inside this test suite for holding the fixtures of the customer input to the createCustomer() function, expected customer output from the same function and also for the error data. Below are these variables declaration added within the above test suite.

var newCustomer, expectedCreatedCustomer, expectedError;

Test Fixture

As you know already, these fixtures are empty, and they need to be filled in with the appropriate content. Based on the customer details’ fields as described in the requirements post, below is the sample content for the new-customer.json fixture file and it is assigned to the newCustomer variable. These attributes value could be different though, as you wish.

{
  "firstName": "John",
  "lastName": "Smith",
  "email": "john.smith@example.com",
  "phoneNumber": "9876543210",
  "address": "100 E Street",
  "city": "New York",
  "state": "NY",
  "zipCode": "10000",
  "country": "USA"
}

Since MongoDB adds a new attribute ‘_id‘ which is of 12 bytes ObjectId format to the customer document as the primary key when it’s created, the expectedCreatedCustomer needs to have the same attributes as the input, newCustomer, along with the additional _id attribute. So below will be the content for the created-customer.json. Here the ‘__v’ is also another MongoDB generated attribute for the document’s version.

{
  "_id": "5a5ea8fde43c771e4aa5ea06",
  "firstName": "John",
  "lastName": "Smith",
  "email": "john.smith@example.com",
  "phoneNumber": 9876543210,
  "address": "100 E Street",
  "city": "New York",
  "state": "NY",
  "zipCode": "10000",
  "country": "USA",
  "__v": 0
}

Test Script

The first spec for the createCustomer() would be added next. Here is the spec for the successful path for the createCustomer() method call. 

it('should successfully create new customer', function () {

});

Let’s fill in the content in this spec now. First, start with obtaining the test data for the new customer and expected customer result variables from the fixtures. 

newCustomer = CustomerFixture.newCustomer;
expectedCreatedCustomer = CustomerFixture.createdCustomer;

In this test, you will be testing the successful creation of the new customer. To do this test, you will use the CustomerModelMock that was created above. In this mock, you need to define the behavior of the function that will be used to create the customer document in the collection.

The mongoose’s Model constructor has create() function, which requires the new customer object as per the CustomerSchema definition as input and returns a promise. Once the customer document is created successfully it will return the newly created document to the caller which can be processed by the function that’s passed to the Promise’s then function.

Here is the expectation of the successful creation of the customer document with the mock CustomerModelMock. The expectation is defined such a way that, mock has a method called ‘create’ with the customer object as a parameter and then it returns a promise function with created customer document once the promise is resolved.

CustomerModelMock.expects('create')
    .withArgs(newCustomer)
    .resolves(expectedCreatedCustomer);

Now comes the central piece of code for this test. As per the low-level design and sequence diagram, CustomerService has to have a method called createCustomer() which in turn calls the CustomerModel’s create() function to create the new customer in the collection. You already defined the CustomerModel’s mock object, CustomerModelMock, with a method and it’s parameter and the return value.

So, let’s invoke the createCustomer() method of the CustomerService and verify all the expectations on the mock with the below code block.

return CustomerService.createCustomer(newCustomer)
    .then(function (data) {
        CustomerModelMock.verify();
        expect(data).to.deep.equal(expectedCreatedCustomer);
    });

Since this spec is for testing the successful path, let’s make use of the promise’s ‘then’ function call. Inside this function, you will be verifying the mock’s expectations with the ‘CustomerModelMock.verify()’ call. Also, the created customer document, which is returned from the createCustomer() call, is confirmed to be equivalent to the expectedCreatedCustomer value.

Running this test suite now will show the new spec is failing as we haven’t written the functional code for createCustomer() yet.

Unit Test 11: CustomerService - CreateCustomer() - Success Test Case - Failed
Unit Test 11: CustomerService – CreateCustomer() – Success Test Case – Failed

Functional Code

Let’s solve the above error by adding an empty function and expose it via the createCustomer() attribute for the module.exports object to the customer.service.js as shown below.

(function () {
    'use strict';

    module.exports = {
        createCustomer: createCustomer
    };

    function createCustomer(customer) {
       
    }

})();

After adding this code block, the initial error would have gone away. However, a new exception will be thrown out as below.

Unit Test 11: CustomerService - CreateCustomer() Success Test Case - Failed 2
Unit Test 11: CustomerService – CreateCustomer() Success Test Case – Failed 2

This error is because of the createFunction() did not return a promise as per the spec’s expectation. Invoking the CustomerModel.create() method and returning it will resolve this issue.

At first, get the CustomerModel from the CustomerModule file by requiring it. Just below the module.exports object assignment.

var CustomerModel = require('./customer.module')().CustomerModel;

Then add the below statement inside the createCustomer() function. This function will call the create method of the CustomerModel and returns the promise which comes from the latter method.

function createCustomer(customer) {
    return CustomerModel.create(customer);
}

With this change, the functional code for creating new customer’s successful path of execution is completed as will be shown in the latest test run’s successful result.

Unit Test 11: CustomerService - CreateCustomer() Success Test Case - Passed
Unit Test 11: CustomerService – CreateCustomer() Success Test Case – Passed

Unit Test 12: CreateCustomer Failure Test Case

This spec is to test the failure path of the createCustomer() function. Here the expectation would be that the function mentioned earlier will throw an exception while creating the new customer.

Test Fixture

At this juncture, we still the have the empty content for the error-unknown.json file, which we are trying to throw for this spec. Let’s complete this file content before we continue with the final step of this test spec. We can assume that there will be some error/exception thrown out of the createCustomer(). So, the error could be in any format. To emulate this behavior, without any need to worry about the exception’s data structure, we could define that this createCustomer() throws an unknown error with the below content for the testing purpose.

{
  "error": "Unknown",
  "message": "Unknown Error"
}

Test Script

You could write the spec for testing the create customer failure as below. Just like the spec for success path, you can start with assigning the test data fixtures for both the input and expected output. Here, the expected output will be an error/exception as we are testing the flow where the create customer function is expected to fail with an exception thrown.

it('should throw error while creating customer', function () {
    expectedError = ErrorFixture.unknownError;
    newCustomer = CustomerFixture.newCustomer;

    CustomerModelMock.expects('create')
        .withArgs(newCustomer)
        .rejects(expectedError);

    return CustomerService.createCustomer(newCustomer)
        .catch(function (error) {
            CustomerModelMock.verify();
            expect(error).to.deep.equal(expectedError);
        });
});

The next step would be to set up the behavior and expectation of the function being tested. As you can see in the above code block, we are mocking the ‘create’ function of the CustomerModelMock with newCustomer as the input for the mocked function and finally making it to reject with an error. It will mock the promise’s behavior of throwing an exception. 

At the final step, we are invoking the CustomerService’s createFunction() method with appropriate input, the new customer test fixture, and handling the thrown exception from the CustomerModelMock’s create method with the ‘catch’ block’s anonymous function. This block contains the code to verify all the expectations are correct or not. Also, also, the thrown error is checked against the expectedError object to complete the spec. 

If we run the mocha test runner now, it will show that all the specs are running successfully, without any more code changes to this spec. That’s because the functional code for both success and failure paths are already completed as the CustomerModel’s create function is returning a promise. So the failed test case is also passing with the same returned promise with the catch function of the promise.

Unit Test 12: CustomerService - CreateCustomer() Failure Test Case - Passed
Unit Test 12: CustomerService – CreateCustomer() Failure Test Case – Passed

Please check out the index page for this Node.js RESTful API development with TDD approach tutorial with all the posts in sequence in one place.

This blog post is an excerpt from the book Building Node.js REST API with TDD approach. Please check out the link for more information.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.