DEV Community

Cover image for Testing Mongoose with Ts-Jest
BERAT DİNÇKAN
BERAT DİNÇKAN

Posted on

Testing Mongoose with Ts-Jest

If you want to learn MongoDB with mongoose, learning by testing is just for you. In this blog post, I talk about how to install ts-jest , how to create models and fake data using typescript and @faker-js/faker, and how to use jest to test them.

Why testing is important ?

Testing the code that We write makes us aware of the possible problems that occur in the future or gives us an idea about the behavior of the code. For instance, We have a car model and the car model has a field named age.The age field can not be negative. At this point, We need to be sure what happens when the age is a negative value. We give a negative input for the age field to the car model then we expect the car model throws an error in a testing module. So we can be sure if the car model works in line with the purpose before deploying the project.

What is jest?

Jest is a javascript testing framework. I will test all models by using jest. The reason I use the jest framework is that it requires minimum configuration for testing.

Creating the project and installing the packages

Creating the package.json



npm init -y


Enter fullscreen mode Exit fullscreen mode

I will use ts-jest package in this blog post beacause ts-jest lets me use jest to test projects written in typescript.

Installing the packages.



npm install -D jest typescript ts-jest @types/jest ts-node @types/node


Enter fullscreen mode Exit fullscreen mode

While installing mongoose we don't need the @types/mongoose because the mongoose package has built-in Typescript declarations.



npm install mongoose


Enter fullscreen mode Exit fullscreen mode

Giving data to inputs by myself is hard so I install the @faker-js/faker package. @faker-js/faker helps me create random data for the models.



npm install -D @faker-js/faker


Enter fullscreen mode Exit fullscreen mode

Creating tsconfig.json



tsc --init


Enter fullscreen mode Exit fullscreen mode

Changing properties in tsconfig.json for the project



 "rootDir": "./src",
 "moduleResolution": "node",
 "baseUrl": ".",
 "outDir": "./build",


Enter fullscreen mode Exit fullscreen mode

Adding include and exclude sides in tsconfig.json.



"include": ["src/**/*.ts"],
"exclude": ["node_modules","build"]


Enter fullscreen mode Exit fullscreen mode

Creating config file for testing



npx ts-jest config:init


Enter fullscreen mode Exit fullscreen mode

After that, You could see jest.config.js in the project folder. And that's it. We are ready to go.

Project Structure

I create two main folders named src and test because I accept this project as a real one. Model files will be in models folder in the src but tests of the models will be in the test.

Connecting the MongoDB

I Create the connectDBForTesting.ts in the test folder. My MongoDB runs on localhost:27018 if you have different options you could add or change connection options while you connect to MongoDB.



touch test/connectDBForTesting.ts


Enter fullscreen mode Exit fullscreen mode

test/connectDBForTesting.ts



import mongoose from "mongoose";

export async function connectDBForTesting() {
  try {
    const dbUri = "mongodb://localhost:27018";
    const dbName = "test";
    await mongoose.connect(dbUri, {
      dbName,
      autoCreate: true,
    });
  } catch (error) {
    console.log("DB connect error");
  }
}

export async function disconnectDBForTesting() {
  try {
    await mongoose.connection.close();
  } catch (error) {
    console.log("DB disconnect error");
  }
}


Enter fullscreen mode Exit fullscreen mode

Creating mongoose model

Models in mongoose are used for creating, reading, deleting, and updating the Documents from the MongoDB database. Let's create and test a Person model.



touch src/models/person.model.ts


Enter fullscreen mode Exit fullscreen mode

src/models/person.model.ts



import mongoose, { Types, Schema, Document } from "mongoose";

export interface PersonInput {
  name: string;
  lastName: string;
  address: string;
  gender: string;
  job: string;
  age: number;
}

export interface PersonDocument extends PersonInput, Document {
  updatedAt: Date;
  createdAt: Date;
}

const PersonSchema = new mongoose.Schema<PersonDocument>(
  {
    name: { type: String, required: [true, "name required"] },
    lastName: { type: String },
    address: { type: String, required: [true, "address required"] },
    gender: { type: String, required: [true, "gender is required"] },
    job: { type: String },
    age: { type: Number, min: [18, "age must be adult"] },
  },
  {
    timestamps: true, // to create updatedAt and createdAt
  }
);

const personModel = mongoose.model("Person", PersonSchema);
export default personModel;


Enter fullscreen mode Exit fullscreen mode

We have 2 important things here, PersonInput and PersonDocument interfaces. The PersonInput interface is used to create the personModel and the PersonDocument interface describes the object that is returned by the personModel. You will see clearly in the test section of the personModel.

Creating test for the personModel



touch test/person.model.test.ts


Enter fullscreen mode Exit fullscreen mode

test/person.model.test.ts



import {
  connectDBForTesting,
  disconnectDBForTesting,
} from "../connectDBForTesting";

import personModel, {
  PersonDocument,
  PersonInput,
} from "../../src/models/person.model";
import faker from "@faker-js/faker";
describe("personModel Testing", () => {
  beforeAll(async () => {
    await connectDBForTesting();
  });

  afterAll(async () => {
    await personModel.collection.drop();
    await disconnectDBForTesting();
  });
});


Enter fullscreen mode Exit fullscreen mode

First of all, the describe creates a block that includes test sections. You can add some global objects in the describe block to use them.

beforeAll runs a function before all tests in the describe block run. In the beforeAll, I connect the MongoDB server.

afterAll runs a function after all tests in the describe block have complated. In the afterAll, I disconnect the MongoDB server and drop the personModel collection.

PersonModel Create Test



test("personModel Create Test", async () => {
  const personInput: PersonInput = {
    name: faker.name.findName(),
    lastName: faker.name.lastName(),
    age: faker.datatype.number({ min: 18, max: 50 }),
    address: faker.address.streetAddress(),
    gender: faker.name.gender(),
    job: faker.name.jobTitle(),
  };
  const person = new personModel({ ...personInput });
  const createdPerson = await person.save();
  expect(createdPerson).toBeDefined();
  expect(createdPerson.name).toBe(person.name);
  expect(createdPerson.lastName).toBe(person.lastName);
  expect(createdPerson.age).toBe(person.age);
  expect(createdPerson.address).toBe(person.address);
  expect(createdPerson.gender).toBe(person.gender);
  expect(createdPerson.job).toBe(person.job);
});


Enter fullscreen mode Exit fullscreen mode

Note : When a new personModel is declared it returns a PersonDocument type object. So I can use the mongoose.Document properties, validates, and middlewares.

I create a person object using personInput. The person.save() method inserts a new document into the database and return PersonDocument type object.

expect checks if the given data matches the certain conditions or not. If the given data matches the certain conditions the test passes. If not so, the test fails.

The last state of the test/models/person.model.test.ts



import {
  connectDBForTesting,
  disconnectDBForTesting,
} from "../connectDBForTesting";

import personModel, {
  PersonDocument,
  PersonInput,
} from "../../src/models/person.model";
import faker from "@faker-js/faker";
describe("personModel Testing", () => {
  beforeAll(async () => {
    await connectDBForTesting();
  });
  afterAll(async () => {
    await personModel.collection.drop();
    await disconnectDBForTesting();
  });

  test("personModel Create Test", async () => {
    const personInput: PersonInput = {
      name: faker.name.findName(),
      lastName: faker.name.lastName(),
      age: faker.datatype.number({ min: 18, max: 50 }),
      address: faker.address.streetAddress(),
      gender: faker.name.gender(),
      job: faker.name.jobTitle(),
    };
    const person = new personModel({ ...personInput });
    const createdPerson = await person.save();
    expect(createdPerson).toBeDefined();
    expect(createdPerson.name).toBe(person.name);
    expect(createdPerson.lastName).toBe(person.lastName);
    expect(createdPerson.age).toBe(person.age);
    expect(createdPerson.address).toBe(person.address);
    expect(createdPerson.gender).toBe(person.gender);
    expect(createdPerson.job).toBe(person.job);
  });
});


Enter fullscreen mode Exit fullscreen mode

Running the jest

I add a command to the scripts in package.json to run the jest.



"scripts": {
    "test": "npx jest --coverage "
  },


Enter fullscreen mode Exit fullscreen mode

coverage options indicates that test coverage information should be collected and reported in the output. But you can ignore it.

I run the test.



npm run test


Enter fullscreen mode Exit fullscreen mode

The test result

result of the test

To see what happens when a test fails I change a expect side with a wrong data on purpose.



expect(createdPerson.job).toBe(person.name);


Enter fullscreen mode Exit fullscreen mode

The result of the test failing

The result of the test failing

The reason the test fails is that the jest expects the createdPerson.job and createdPerson.name to have the same data.

PersonModel Read Test



test("personModel Read Test", async () => {
  const personInput: PersonInput = {
    name: faker.name.findName(),
    lastName: faker.name.lastName(),
    age: faker.datatype.number({ min: 18, max: 50 }),
    address: faker.address.streetAddress(),
    gender: faker.name.gender(),
    job: faker.name.jobTitle(),
  };
  const person = new personModel({ ...personInput });
  await person.save();
  const fetchedPerson = await personModel.findOne({ _id: person._id });
  expect(fetchedPerson).toBeDefined();
  expect(fetchedPerson).toMatchObject(personInput);
});


Enter fullscreen mode Exit fullscreen mode

I create a personModel and save it then fetch the person by _id. The fetchedPerson has to be defined and its properties have to be the same as the personInput has. I can check if the fetchPerson properties match the personInput properties using the expect.tobe() one by one but using expect.toMatchObject() is a little bit more easy.

expect.toMatchObject() checks if a received javascript object matches the properties of an expected javascript object.

Something is missing

For the each test, I created person model over and over again.It was not much efficient Therefore I declare the personInput and personModel top of the describe.



describe("personModel Testing", () => {}
const personInput: PersonInput = {
    name: faker.name.findName(),
    lastName: faker.name.lastName(),
    age: faker.datatype.number({ min: 18, max: 50 }),
    address: faker.address.streetAddress(),
    gender: faker.name.gender(),
    job: faker.name.jobTitle(),
  };
  const person = new personModel({ ...personInput });
)


Enter fullscreen mode Exit fullscreen mode

So I can use the personInput and person objects in all tests.

PersonModel Update Test



test("personModel Update Test", async () => {
  const personUpdateInput: PersonInput = {
    name: faker.name.findName(),
    lastName: faker.name.lastName(),
    age: faker.datatype.number({ min: 18, max: 50 }),
    address: faker.address.streetAddress(),
    gender: faker.name.gender(),
    job: faker.name.jobTitle(),
  };
  await personModel.updateOne({ _id: person._id }, { ...personUpdateInput });
  const fetchedPerson = await personModel.findOne({ _id: person._id });
  expect(fetchedPerson).toBeDefined();
  expect(fetchedPerson).toMatchObject(personUpdateInput);
  expect(fetchedPerson).not.toMatchObject(personInput);
});


Enter fullscreen mode Exit fullscreen mode

Even if I use the same schema, I can create personUpdateInput that is different from personInput because @faker-js/faker creates data randomly. The properties of fetchedPerson is expected to match the personUpdateInput at the same time is expect to not match the personInput.

PersonModel Delete Test



test("personModel Delete Test", async () => {
  await personModel.deleteOne({ _id: person._id });
  const fetchedPerson = await personModel.findOne({ _id: person._id });
  expect(fetchedPerson).toBeNull();
});


Enter fullscreen mode Exit fullscreen mode

I delete a mongoose document by using person._id. After that, The fetchedPerson fetched from MongoDB by using is expected to be null.

The last State of the test/models/person.model.test.ts



import {
connectDBForTesting,
disconnectDBForTesting,
} from "../connectDBForTesting";

import personModel, {
PersonDocument,
PersonInput,
} from "../../src/models/person.model";
import faker from "@faker-js/faker";
describe("personModel Testing", () => {
const personInput: PersonInput = {
name: faker.name.findName(),
lastName: faker.name.lastName(),
age: faker.datatype.number({ min: 18, max: 50 }),
address: faker.address.streetAddress(),
gender: faker.name.gender(),
job: faker.name.jobTitle(),
};
const person = new personModel({ ...personInput });

beforeAll(async () => {
await connectDBForTesting();
});
afterAll(async () => {
await personModel.collection.drop();
await disconnectDBForTesting();
});

test("personModel Create Test", async () => {
const createdPerson = await person.save();
expect(createdPerson).toBeDefined();
expect(createdPerson.name).toBe(person.name);
expect(createdPerson.lastName).toBe(person.lastName);
expect(createdPerson.age).toBe(person.age);
expect(createdPerson.address).toBe(person.address);
expect(createdPerson.gender).toBe(person.gender);
expect(createdPerson.job).toBe(person.job);
});

test("personModel Read Test", async () => {
const fetchedPerson = await personModel.findOne({ _id: person._id });
expect(fetchedPerson).toBeDefined();
expect(fetchedPerson).toMatchObject(personInput);
});
test("personModel Update Test", async () => {
const personUpdateInput: PersonInput = {
name: faker.name.findName(),
lastName: faker.name.lastName(),
age: faker.datatype.number({ min: 18, max: 50 }),
address: faker.address.streetAddress(),
gender: faker.name.gender(),
job: faker.name.jobTitle(),
};
await personModel.updateOne({ _id: person._id }, { ...personUpdateInput });
const fetchedPerson = await personModel.findOne({ _id: person._id });
expect(fetchedPerson).toBeDefined();
expect(fetchedPerson).toMatchObject(personUpdateInput);
expect(fetchedPerson).not.toMatchObject(personInput);
});

test("personModel Delete Test", async () => {
await personModel.deleteOne({ _id: person._id });
const fetchedPerson = await personModel.findOne({ _id: person._id });
expect(fetchedPerson).toBeNull();
});
});

Enter fullscreen mode Exit fullscreen mode




Testing all




npm run test

Enter fullscreen mode Exit fullscreen mode




Result

Result
That's it. This is usually how to test mongoose models:

  • create a mongoose model.
  • create a test for the mongoose model.
  • apply the CRUD operations for the mongoose model in test sections.
  • if test fails, try to find out and solve the problem.
  • if the all tests pass, you are ready to go.

Sources:

Contact me:

Github Repo: https://github.com/pandashavenobugs/testing-mongoose-with-tsjest-blogpost

Top comments (1)

Collapse
 
cybert33n profile image
(ᑕƳᗷᗴᖇ丅33ᑎ)

This is actually not the way to go. You mock it or you use in memory mongodb. The reason behind it is that you can run test parallel without the risk that tests corrupt each other in the same database with same documents