Implementing and Testing a Mongoose Model with CI/CD Integration

Implementing and Testing a Mongoose Model with CI/CD Integration

If you are building a server-side Node application with MongoDB as your choice of database, it is quite likely that you are using an Object Document Mapper (ODM) like Mongoose to model your application data. In this article, we will walk through a simple example of setting up, implementing, and writing test code for a Mongoose Model. We will also look into how you can easily automate this development workflow with CI/CD integration using Buddy.

The code for this example can be found here.

Installation and setup

Before getting started with this example, make sure you have Node, MongoDB, and Git installed. For the CI/CD integration, you will need to push your code to a remote repository on GitHub or BitBucket, and also have an account on Buddy.

Setting up the initial code

First, initialize a package.json file in your project folder by running npm init from the command line. Once you have answered the prompted questions, and generated the package.json file, we can install the necessary npm modules.

Mongoose

In order to use Mongoose in our code we will install the mongoose npm module by running the command:

npm install --save mongoose

Ensure MongoDB is running on your system, so the application code can connect to it.

Jest

We will use Jest, a JavaScript Testing Framework, to write and run our tests. For this we install the jest npm module as a dev dependency.

npm install --save-dev jest

We will add a test script to package.json, so we can run the tests with the npm run test command.

// File: package.json
  "scripts": {
    "test": "jest"
  },

By default, Jest assumes that the tests will run in a browser environment, but since our tests will be running in the node environment, we need to configure Jest to specify this. In a jest.config.js file, we will add the following configuration:

// File: jest.config.js
module.exports = {
    testEnvironment: 'node' 
}

Finally we will create a __tests__ folder in the project directory, to house our test files, and Jest will look in this folder for tests to run.

Writing the first test

We start with an empty test file named user.model.test.js. This will contain the test suite for a User Model that we will implement in the rest of this example.

In this test suite, we first connect to Mongodb using Mongoose.

// File: __tests__/user.model.test.js
const mongoose = require('mongoose')
mongoose.Promise = global.Promise
const dbtype = process.env.dbtype ? 'mongo' : 'localhost'
mongoose.connect('mongodb://' + dbtype + '/testUser', {
  useNewUrlParser: true
})
mongoose.connection.on('error', () => {
  throw new Error(`unable to connect to database`)
})

We construct the database URL conditionally with localhost for running the code locally and with mongo to run it in the CI/CD environment. When we set up on Buddy, we will set the environment variable dbtype, so the code runs using the configured MongoDB service on Buddy.

With the MongoDB connection code ready, we will add an initial passing test which will help us check if the code is set up correctly.

// File: __tests__/user.model.test.js
describe("initial test", () => {
  it("runs successfully", () => {
    expect(true).toEqual(true)
  })
})

We will also add an afterAll block, where we close the database connection after all the tests in the suite have run:

// File: __tests__/user.model.test.js
afterAll(async () => {
  try {
    await mongoose.connection.close()
  } catch (err) {
    console.log(err)
  }
})

Finally, run the test from the command line with npm run test, and if it passes, this will confirm the setup is done.

Pushing to a remote repository

Create a repository on GitHub if you haven’t already, then commit and push this setup code with the first test, so we can integrate this example with a CI/CD pipeline on Buddy.

CI/CD integration

Whether you are collaborating with a large team or on an application already in production, automating the repetitive steps in your software delivery process, like building, testing, and deploying your code, can speed up your product iterations and help you deliver bug-free code. Continuous Integration and Continuous Deployment tools help to achieve just this.

Next, we will see how to set up a CI/CD pipeline on Buddy that will automatically build and test our Mongoose Model example as we incrementally develop and push the code.

Connecting a remote repository to Buddy

To get started, create a new project from your Buddy account, as outlined below.

First, start by adding a new project:

New buddy project

Next, select the remote Git hosting provider where you have your code repository.

Connect rep

Then, find and select your repository.

Attach git repo

Buddy will automatically identify the Node.js application in your repository, and give you the option to add a new CI/CD pipeline for this application.

Automate node

A pipeline is a set of actions performed on the repository code, such as builds or tests.

Adding a pipeline

We will add a simple CI/CD pipeline that will build our code and run the tests on every code push to the remote repository. While adding a new pipeline, Buddy lets us set the trigger mode, and which branch of code it is applicable to.

Trigger modes:

  • Manual - lets us run the pipeline manually
  • On push - runs the pipeline on every code push
  • Recurrently - runs the pipeline at a specified time interval

Add pipeline trigger mode

In order to configure this pipeline, so it can build and test our code, we first need to add a Node.js action.

Node action

As our application uses MongoDB, next we need to add a MongoDB service from Services:

Select mongodb service

Finally, we need to configure the environment variable dbtype, so the code correctly constructs the database URL when running in this pipeline on Buddy.

Add variable

We do this by adding a new variable in the Variables section, specifying the key as 'dbtype'.

dbtype variable

With these configuration steps completed, the new pipeline is ready to run the code already committed and pushed to the remote repository.

Run pipeline Clicking on Run pipeline will give us the option to select the commit to run.

Run specific commit

Once the pipeline is running, it will show build in progress.

Run in progress

Then it will show that the build either passed or failed.

Build passed

We can check the Logs to see the details of a passed or failed build, as shown below in an example failed build.

Build failed logs

Now that the CI/CD pipeline is set up and integrated with the code repository, we will continue developing the Mongoose User Model, and on every code push the pipeline can automatically build and run the tests for us.

Mongoose User Model

We will implement a basic user model using Mongoose, with the schema containing a username field and an email field, where username is a required field and email must be unique and valid. We will write tests to check if schema fields are defined as expected, if a new user object is being created successfully, and if the fields are validated correctly.

Defining a user schema

We will define the user schema using Mongoose.Schema in user.model.js:

// File: user.model.js
const mongoose = require('mongoose')

const UserSchema = new mongoose.Schema({
  username: {
    type: String
  },
  email: {
    type: String
  }
})

module.exports = mongoose.model ('User', UserSchema)

We begin by simply defining two fields, username and email, which will store values of type String. We can use the exported User model to create new user documents, and query the User collection in the database.

In order to test if the User schema defines the attributes we want for every user, we will add our first test for the User Model test suite. In user.model.test.js, we will replace the initial test with a describe block for User Model tests, and add a test to check if the user schema contains the username and email attributes.

// File: __tests__/user.model.test.js
const User = require('./../user.model.js')
...
describe("User Model", () => {
  it("has username and email attributes", () => {
    let expectedKeys = ["username", "email"]
    let keys = Object.keys(User.schema.paths)
    let userAttributes = [keys[0], keys[1]]
    expect(userAttributes).toStrictEqual(expectedKeys)
  })
})

We retrieve the keys defined in the User model by calling Object.keys(User.schema.paths), and compare it against our expected keys. We can run the test locally with Jest to check whether it passes.

Committing and pushing this User Model definition code and the first test will trigger the CI/CD pipeline on Buddy to run - building and testing this code.

Creating a user object

Next, we will write a test for checking if a user document is being created successfully when provided a username and email on save:

// File: __tests__/user.model.test.js
  it("should create a new user", async () => {
    try {
      const user = new User({
        username: "john",
        email: "john@smith.info"
      })
      let result = await user.save()
      expect(result.username).toEqual(user.username)
      expect(result.email).toEqual(user.email)
    } catch (err) {
      throw new Error(err)
    }
  })

We also need to clear the User collection after running each test in this test suite, so the same user is not created multiple times when we save new users in other tests or re-run the tests. For this purpose, we will add an afterEach block that deletes all the documents in the User collection after each test.

// File: __tests__/user.model.test.js
afterEach(async () => {
  try {
    await User.deleteMany({})
  } catch (err) {
    console.log(err)
  }
})

Validating user attributes

In this section, we will add validation to the fields and write tests to ensure the code is working as expected.

Required username

We will make the username field a required attribute in the User schema, so that a user is not saved in the database if it does not contain a value for the username field.

// File: user.model.js
username: {
    type: String,
    required: "username is required"
},

Then we will write a test to ensure the code throws a validation error of type 'required' on the username attribute, when we try to save a user with an empty username.

// File: __tests__/user.model.test.js
  it("should throw an error if the username field is empty", async () => {
    try {
      await new User({
        username: "",
        email: "john@smith.info"
      }).save()
    } catch (err) {
      expect(err.errors.username.kind).toEqual("required")
    }
  })

Unique user

The User model should not allow multiple users with the same email address. We will update the email field in the User schema to add a unique validator.

// File: user.model.js
email: {
    type: String ,
    unique: 'email already exists'
}

To check this validation, we will add a test that tries to save two users with the same email address. The expectation in this case, is that an error will be thrown by Mongoose with the error code 11000, which translates to a unique validation error.

// File: __tests__/user.model.test.js
  it("should throw an error on save if two users use the same email", async () => {
    try {
      await new User({
        username: "sam",
        email: "sam@ed.info"
      }).save()
      await new User({
        username: "tom",
        email: "sam@ed.info"
      }).save()
    } catch (err) {
      expect(err.code).toEqual(11000)
    }
  })

Valid email addresses

Finally, we will add a validation that will ensure users are saved with valid email addresses. We will update the User schema again to add a match pattern to the email field. If the provided email value does not match the given regex pattern for a valid email, a corresponding error message will be returned.

// File: user.model.js
  email: {
    type: String,
    unique: 'email already exists',
    match: [/.+\@.+\..+/, 'Please give a valid email address']
  },

We will test this email validation code by defining a test that tries to save a new user with an invalid email. This should generate an error that contains the relevant error message. We will catch this error and check the error message.

// File: __tests__/user.model.test.js
  it("should throw an error if the email is invalid", async () => {
    try {
      await new User({
        username: "john",
        email: "johnsmith.info"
      }).save()
    } catch (err) {
      expect(err.errors.email.message).toEqual("Please give a valid email address")
    }
  })

These five test cases cover the simple features we had decided on for our implementation of the basic User Model.

All tests

While working through this example, we looked at how to:

  • set up a Node application and a Jest test suite that will need to connect to a MongoDB database using Mongoose,
  • integrate a CI/CD pipeline on Buddy with a Node and MongoDB application,
  • implement a basic Mongoose model, and
  • write tests to verify the features of the Mongoose model.

About the Author
Shama Hoque

Shama Hoque

Shama Hoque is a software developer, author, and mentor with more than 9 years of experience. Currently, she makes web-based prototypes for R&D startups in California, while training aspiring software engineers and teaching web development to CS undergrads in Bangladesh. She is the author of Packt's Full-Stack React Projects book.

The Web Dev Monthly

Sign up for a free monthly scoop of news and features articles handpicked by our staff.

Unsubscribe at any time. No hidden catch.