“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.