PART II: write unit & integration tests with Mongoose and Typescript.

“NOTHING WILL WORK UNLESS YOU DO.”

–MAYA ANGELOU

In the first part, we described the different steps to create a server with Node.js, Typescript and mongoose of our users-demo application.

Feel free to check the part I if you missed it.

In this post, we will present the unit and integration tests implemented for the different apis of our app.

First, we have to install some packages that we will need for testing:

$pc> npm i jest supertest  ts-jest  @types/jest  @types/supertest -D

jest: is a javascript testing framework created by Facebook and it is also used to test react components.

supertest: a module used to test http endpoints.

ts-jest: is a TypeScript preprocessor that lets you use Jest to test projects written in TypeScript.

Then, we add a new script in package.json in order to use it later when we need to run the different tests of our app:

"scripts": {
    "start:build": "tsc -w",
    "start:run": "nodemon ./build/server/server.js",
    "start": "set NODE_ENV=development && concurrently npm:start:*",
    "test": "jest "
  },

Jest looks for the files which have the suffix .spec.ts or test.ts under the __tests__ folder to compile them.

So, we create now the __tests__ folder under src and we will put all our test files there.

We start by writing unit tests of our database, we create a file db.unit.spec.ts, then we implement a test to mock the creation of a new user in the database:

import User from "../server/models/User";

describe("Create users", () => {
  it("Should create a new user successfully!", () => {
    const mockUser = {
      address: "user address",
      email: "user@gmail.com",
      name: "John",
    };
    const spy = jest.spyOn(User, "create").mockReturnValueOnce(mockUser as any);
    User.create(mockUser);
    const spyCreatedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyCreatedUser.name).toEqual(mockUser.name);
    spy.mockReset();
  });

In this first test, we mock the mongoose's creation api using the jest.spyOn() function provided by jest. It takes two arguments which are the object and methodName and returns a jest mock function.

In our case, we need to mock the returned value by jest.spyOn(), so we used the function mockReturnValueOnce(value) to return a mocked value once.

Then, we call the original function in order to be spied and we get the returned result from the array spy.mock.results which contains the calls of our mocked function.

spy.mock.results is an array of objects, each object has the properties type and value.

The property type can be one of these following options:

  • complete

  • throw

  • incomplete

In our case, we focus on the complete type which indicates the call completed by returning normally.

Then, we check if our spy function was called, and the returned value equals to the mocked created user.

Finally, we call mockReset() which resets all information stored in the spy.mock.calls and removes the mocked return values.

We use the same things to continue testing the different functionalities in the db.unit.spec.ts files:

import { Types } from "mongoose";
import User from "../server/models/User";

describe("Create users", () => {
  it("Should create a new user successfully!", () => {
    const mockUser = {
      address: "user address",
      email: "user@gmail.com",
      name: "John",
    };
    const spy = jest.spyOn(User, "create").mockReturnValueOnce(mockUser as any);
    User.create(mockUser);
    const spyCreatedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyCreatedUser.name).toEqual(mockUser.name);
    spy.mockReset();
  });

  it("Should retruns an error when the name is missing", () => {
    const mockUser = {
      address: "user address",
      email: "user@gmail.com",
    };
    const spy = jest.spyOn(User, "create").mockReturnValueOnce("Name is required" as any);
    User.create(mockUser);
    const spyCreatedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyCreatedUser).toEqual("Name is required");

    spy.mockReset();
  });

  it("Should retruns an error when the email is missing", () => {
    const mockUser = {
      address: "user address",
      name: "John",
    };
    const spy = jest.spyOn(User, "create").mockReturnValueOnce("Email is required" as any);
    User.create(mockUser);
    const spyCreatedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyCreatedUser).toEqual("Email is required");
    spy.mockReset();
  });
});

describe("READ users", () => {
  it("Should return the list of users successfully", () => {
    const mockedUserList = [
      {
        _id: Types.ObjectId(),
        address: "user address 1",
        email: "john@gmail.com",
        name: "John",
      },
      {
        _id: Types.ObjectId(),
        address: "user address 2",
        email: "smith@gmail.com",
        name: "Smith",
      },
    ];

    const spy = jest.spyOn(User, "find").mockReturnValueOnce(mockedUserList as any);
    User.find({});

    const spyFetchedUsers = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyFetchedUsers).toHaveLength(2);
    spy.mockReset();
  });

  it("Should return an empty list if there are no user", () => {
    const spy = jest.spyOn(User, "find").mockReturnValueOnce([] as any);
    User.find({});

    const spyFetchedUsers = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyFetchedUsers).toHaveLength(0);
    spy.mockReset();
  });

  it("Should return a user successfully!", () => {
    const mockUser = {
      _id: Types.ObjectId(),
      address: "user address 1",
      email: "john@gmail.com",
      name: "John",
    };
    const spy = jest.spyOn(User, "findById").mockReturnValueOnce(mockUser as any);
    User.findById(mockUser._id);

    const spyFetchedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyFetchedUser.name).toEqual(mockUser.name);
    spy.mockReset();
  });

  it("Should return an error when the user does not exit", () => {
    const id = Types.ObjectId();
    const spy = jest.spyOn(User, "findById").mockReturnValueOnce("User not found" as any);
    User.findById(id);

    const spyFetchedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyFetchedUser).toEqual("User not found");
    spy.mockReset();
  });
});

describe("UPDATE users", () => {
  it("Should update a user successfully!", () => {
    const mockUser = {
      _id: Types.ObjectId(),
      address: "user address 1",
      email: "john@gmail.com",
      name: "John",
    };
    const spy = jest.spyOn(User, "findByIdAndUpdate").mockReturnValueOnce(mockUser as any);
    User.findByIdAndUpdate(mockUser._id, {
      email: "john@gmail.com",
    }, {
      new: true,
    });

    const spyUpdatedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyUpdatedUser.email).toEqual("john@gmail.com");
    spy.mockReset();
  });

  it("Should returns an error if a user does not exist", () => {
    const spy = jest.spyOn(User, "findByIdAndUpdate").mockReturnValueOnce("Id provided does not match any user" as any);
    User.findByIdAndUpdate(Types.ObjectId(), {
      email: "john@gmail.com",
    }, {
      new: true,
    });

    const spyUpdatedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyUpdatedUser).toEqual("Id provided does not match any user");
    spy.mockReset();
  });
});

describe("DELETE users", () => {
  it("Should delete a user successfully!", () => {
    const mockUser = {
      _id: Types.ObjectId(),
      address: "user address 1",
      email: "john@gmail.com",
      name: "John",
    };
    const spy = jest.spyOn(User, "findByIdAndRemove").mockReturnValueOnce(mockUser as any);
    User.findByIdAndRemove(mockUser._id);

    const spyDeletedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyDeletedUser._id).toEqual(mockUser._id);
    spy.mockReset();
  });

  it("Should returns an error if a user does not exist", () => {
    const spy = jest.spyOn(User, "findByIdAndRemove").mockReturnValueOnce("Id provided does not match any user" as any);
    User.findByIdAndRemove(Types.ObjectId());

    const spyDeletedUser = spy.mock.results[0].value;
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spyDeletedUser).toEqual("Id provided does not match any user");
    spy.mockReset();
  });
});

We move now to the integration tests of the database. In these tests, we use some jest methods in order to start up the connection with the database for testing:

beforeAll(() => {
    db.on("open", () => {
      console.log("Database starts successfully");
    });
  });

beforeAll allows to start the database before any of our tests run. To guarantee that our tests don't be related to each other and get errors, we add this method:

 beforeEach(() => {
    if (db.collection("users").countDocuments()) {
      return db.collection("users").deleteMany({});
    }
  });

to delete all the documents from users collection. Finally, after running the tests, we have to close the database connection:

afterAll(() => {
    return db.close();
  });

Then, we tested the different use cases of the CRUD functions concerning the manipulation of our users into a database:

import { Types } from "mongoose";
import "../server/config";
import db from "../server/database/connection";
import User from "../server/models/User";

describe("Users", () => {
  beforeAll(() => {
    db.on("open", () => {
      console.log("Database starts successfully");
    });
  });
  beforeEach(() => {
    if (db.collection("users").countDocuments()) {
      return db.collection("users").deleteMany({});
    }
  });

  afterAll(() => {
    return db.close();
  });

  // Creation
  describe("User Creation", () => {
    it("Should add a new user to database", async () => {
      const newUser = new User({
        address: "user address",
        email: "user@gmail.com",
        name: "user 1",
      });
      const createdUser = await User.create(newUser);
      expect(createdUser).toEqual(createdUser);
    });

    it("Should add a list of users to database", async () => {
      const users = [
        {
          address: "address 1",
          email: "user1@gmail.com",
          name: "user 1",
        },
        {
          address: "address 2",
          email: "user2@gmail.com",
          name: "user 2",
        },
      ];
      const createdUsers = await User.create(users);
      expect(createdUsers).toHaveLength(2);
    });
    it("Should returns an error if a name is missing", async () => {
      const newUser = new User({
        address: "address",
        email: "user@gmail.com",
      });
      await expect(User.create(newUser)).rejects.toThrow("Path `name` is required");
    });

    it("Should returns an error if a user email is missing", async () => {
      const newUser = new User({
        address: "address",
        name: "user",
      });
      await expect(User.create(newUser)).rejects.toThrow("Path `email` is required");
    });
  });

  // Update
  describe("User Update", () => {
    it("Should update a user successfully", async () => {
      const newUser = new User({
        address: "user address",
        email: "user@gmail.com",
        name: "user 1",
      });
      const updatedEmail = "new_email@gmail.com";
      const { _id } = await User.create(newUser);
      const updatedUser = await User.findByIdAndUpdate(_id, {
        email: updatedEmail,
      }, {
        new: true,
      });

      if (updatedUser) {
        return expect(updatedUser.email).toEqual(updatedEmail);
      }
    });

    it("Should not update unexistant user", async () => {
      const updatedUser = await User.findByIdAndUpdate(Types.ObjectId(), {
        name: "user updated",
      }, {
        new: true,
      });

      expect(updatedUser).toBeNull();
    });
  });

  // Delete
  describe("User Delete", () => {
    it("Should delete a user successfully", async () => {
      const newUser = new User({
        address: "user address",
        email: "user@gmail.com",
        name: "user 1",
      });
      const { _id } = await User.create(newUser);
      const deletedUser = await User.findByIdAndRemove(_id);

      if (deletedUser) {
        expect(deletedUser.name).toBe(newUser.name);
      }

    });

    it("Should not delete unexistant user", async () => {
      const updatedUser = await User.findByIdAndRemove(Types.ObjectId());
      expect(updatedUser).toBeNull();
    });
  });

  // Read
  describe("User Read", () => {
    it("Should return a user successfully", async () => {
      const newUser = new User({
        address: "user address",
        email: "user@gmail.com",
        name: "user 1",
      });
      const { _id } = await User.create(newUser);
      const returnedUser = await User.findOne(_id);
      if (returnedUser) {
        expect(returnedUser.name).toBe(newUser.name);
      }

    });

    it("Should not retrun unexistant user", async () => {
      const returnedUser = await User.findOne(Types.ObjectId());
      expect(returnedUser).toBeNull();
    });
  });
});

Finally, we test our server apis using the supertest package which helps us to test the http endpoints. We use the three methods that we used before to start the database connection, delete the collection's documents and close the database connection after running our tests.

import { Types } from "mongoose";
import request from "supertest";
import { app } from "../server/app";
import db from "../server/database/connection";

describe("Server Apis", () => {
  beforeAll(() => {
    db.on("open", () => {
      console.log("Database starts successfully");
    });
  });
  beforeEach(() => {
    if (db.collection("users").countDocuments()) {
      return db.collection("users").deleteMany({});
    }
  });

  afterAll(() => {
    return db.close();
  });

  describe("POST /users", () => {
    it("should create a user successfully!!", async () => {
      const mockUser = {
        address: "user address",
        email: "user@gmail.com",
        name: "user",
      };

      const { status, body } = await request(app).post("/users").send(mockUser);

      expect(status).toBe(201);
      expect(body.name).toBe(mockUser.name);
    });

    it("should create a list of users successfully!!", async () => {
      const mockUsers = [{
        address: "user 1",
        email: "user1@gmail.com",
        name: "user 1",
      },
      {
        address: "user 2",
        email: "user2@gmail.com",
        name: "user 2",
      }];

      const { status, body } = await request(app).post("/users").send(mockUsers);

      expect(status).toBe(201);
      expect(body).toHaveLength(2);
    });

    it("should return an error while a user attribute missing", async () => {
      const mockUser = {
        address: "user address",
        email: "user@gmail.com",
      };

      const { status, error } = await request(app).post("/users").send(mockUser);

      expect(status).toBe(500);
      expect(error.text).toMatch("name: Path `name` is required.");
    });
  });

  describe("DELETE /users/:id", () => {
    it("should delete a user successfully!!", async () => {
      const mockUser = {
        address: "user address",
        email: "user@gmail.com",
        name: "user",
      };

      const { _id } = (await request(app).post("/users").send(mockUser)).body;
      const { status, body } = await request(app).delete(`/users/${_id}`);

      expect(status).toBe(200);
      expect(body.name).toBe(mockUser.name);
    });

    it("should return an error while the user does not exist", async () => {
      const { status, error } = await request(app).delete(`/users/${Types.ObjectId()}`);

      expect(status).toBe(404);
      expect(error.text).toMatch("User not found");
    });
  });

  describe("UPDATE /users/:id", () => {
    it("should update a user successfully!!", async () => {
      const mockUser = {
        address: "user address",
        email: "user@gmail.com",
        name: "user",
      };

      const { _id } = (await request(app).post("/users").send(mockUser)).body;
      const { status, body } = await request(app).patch(`/users/${_id}`).send({
        name: "updated user",
      });

      expect(status).toBe(200);
      expect(body.name).toBe("updated user");
    });

    it("should return an error while the user does not exist", async () => {
      const { status, error } = await request(app).patch(`/users/${Types.ObjectId()}`).send({
        name: "updated user",
      });

      expect(status).toBe(404);
      expect(error.text).toMatch("User not found");
    });
  });

  describe("GET /users/:id", () => {
    it("should return a user successfully!!", async () => {
      const mockUser = {
        address: "user address",
        email: "user@gmail.com",
        name: "user",
      };

      const { _id } = (await request(app).post("/users").send(mockUser)).body;
      const { status, body } = await request(app).get(`/users/${_id}`);

      expect(status).toBe(200);
      expect(body.name).toBe(mockUser.name);
    });

    it("should return an error while the user does not exist", async () => {
      const { status, error } = await request(app).get(`/users/${Types.ObjectId()}`);

      expect(status).toBe(404);
      expect(error.text).toMatch("User not found");
    });
  });

  describe("GET /users", () => {
    it("should return a list of users successfully!!", async () => {
      const users = [
        {
          address: "address 1",
          email: "user1@gmail.com",
          name: "user1",
        },
        {
          address: "address 2",
          email: "user2@gmail.com",
          name: "user2",
        },
      ];

      await request(app).post("/users").send(users);
      const { status, body } = await request(app).get("/users");

      expect(status).toBe(200);
      expect(body).toHaveLength(2);
    });

    it("should return an empty array when there are no users", async () => {
      const { status, body } = await request(app).get("/users");

      expect(status).toBe(200);
      expect(body).toHaveLength(0);
    });
  });
});

To run our tests we just need to execute our script:

$pc> npm test

All the tests are green and pass successfully:

> jest

 PASS  build/__tests__/db.unit.spec.js
 PASS  src/__tests__/db.unit.spec.ts    
 PASS  build/__tests__/app.spec.js (7.127s)
  ● Console

    console.log build/__tests__/app.spec.js:49
      Database starts successfully

 PASS  src/__tests__/app.spec.ts (7.453s)
  ● Console

    console.log src/__tests__/app.spec.ts:9
      Database starts successfully

 PASS  build/__tests__/db.spec.js (8.612s)
  ● Console

    console.log build/__tests__/db.spec.js:49
      Database starts successfully

 PASS  src/__tests__/db.spec.ts (8.948s)
  ● Console

    console.log src/__tests__/db.spec.ts:9
      Database starts successfully


Test Suites: 6 passed, 6 total
Tests:       64 passed, 64 total
Snapshots:   0 total
Time:        9.608s, estimated 10s
Ran all test suites.

Summary

In this part, we tried to give the different steps that we used to write some unit and integration tests for our application described in part I using jest framework, supertest and mongoose.

You will find the complete project on users-demo repository on github. If anyone has any suggestions or find any mistake, or anything to be improved or enhanced in this example, feel free to leave me a comment and if you don't have anything to mention don't forget to like this article.

No Comments Yet