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.
javascript// 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:
javascript// 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.
javascript// 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.
javascript// 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:
javascript// 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:
Image loading...
Next, select the remote Git hosting provider where you have your code repository.
Image loading...
Then, find and select your repository.
Image loading...
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.
Image loading...
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
Image loading...
In order to configure this pipeline, so it can build and test our code, we first need to add a Node.js action.
Image loading...
As our application uses MongoDB, next we need to add a MongoDB service from Services:
Image loading...
Finally, we need to configure the environment variable dbtype
, so the code correctly constructs
the database URL when running in this pipeline on Buddy.
Image loading...
We do this by adding a new variable in the Variables section, specifying the key as 'dbtype'.
Image loading...
With these configuration steps completed, the new pipeline is ready to run the code already committed and pushed to the remote repository.
Image loading... Clicking on Run pipeline will give us the option to select the commit to run.
Image loading...
Once the pipeline is running, it will show build in progress.
Image loading...
Then it will show that the build either passed or failed.
Image loading...
We can check the Logs to see the details of a passed or failed build, as shown below in an example failed build.
Image loading...
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
:
javascript// 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.
javascript// 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:
javascript// 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.
javascript// 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.
javascript// 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.
javascript// 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.
javascript// 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.
javascript// 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.
javascript// 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.
javascript// 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.
Image loading...
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.