Better Programming

Advice for programmers.

Follow publication

Test Controllers in NestJS + Mongo With Jest

How to plan and write solid unit tests with Jest for your NestJS controllers when using MongoDB and Mongoose

Vinicius Santos de Pontes
Better Programming
Published in
11 min readMay 2, 2022
Image with Jest, NestJS and MongoDB logo

In this article, you will learn, by example, how to plan and write solid unit tests with Jest for your NestJS controllers when using MongoDB and Mongoose. Most of the testing techniques used here can be applied without NestJS, using other DBMS (Database Management System) or even using a testing library other than Jest! Those are being used here due to their relevance and because of how much work they simplify.

Before jumping into the code, lets see a quick overview of the main libraries used (install them with NPM).

NestJS

A Node.js framework for writing server-side applications with JavaScript or, better yet, TypeScript. NestJS simplifies the architectural part of the development by providing its own out-of-the-box application architectural patterns, aimed to provide scalability, decoupling, easy maintenance, and, of course, testability. The NestJS core weekly downloads at NPM have been increasing consistently, with more than 1.3 million of downloads in the last week.

Graph showing the increase of weekly downloads

Mongo

I used “mongo” to reffer to MongoDB + Mongoose + MongoDB Memory Server. MongoDB is a performant NoSQL DBMS that stores data as JSON-like formatted documents. Mongoose is a JavaScript object modeling library that makes interacting with MongoDB easier and safer. MongoDB Memory Server is a library that spins up an actual/real MongoDB server programmatically from within Node.js.

Jest

Jest is a complete JavaScript testing framework developed by Meta (previously Facebook) with focus on simplicity and performance (tests run in parallel, for example). The Jest weekly downloads at NPM has been increasing consistently, with more than 17.3 million of downloads in the last week.

Graph showing the increase of weekly downloads

Implementation Objective

What do we want to implement and test in this guide? We will implement and test two endpoints in a NestJS controller. A GET endpoint mapped to a method called getArticle and a POST endpoint mapped to a method called postArticle.

The code and project structure used here are simplified to focus on the testing aspects. If you want to see more tests and code in a more complete project context, refer to the following GitHub repository:

This was a challenge from Coodesh. Simply put, the purpose is to make an API capable of interfacing CRUD (Create, Read, Update and Delete) operations on a database of space flight news articles.

From Where To Start?

How would you start to write your code and the tests? If you already have your code done, good! You can write your tests. But I would like to quickly talk about a good practice know as Test Driven Development (TDD).

In TDD, we follow these three rules:

1- You should not write any production code before writing an unit test that ensures the correct behavior.

2- You should not write more tests than enough to the failure.

3- You should not write more production code than enough to pass the test.

These rules lead us to the following workflow:

Blocks with descriptions linked in the following order: TDD, Write Test, Write Code That Passes The Test and Refactor. The “Refactor” block has a dotted line pointing back to “Write Test”.
TDD workflow

First, we write the tests that ensure the correct behavior of the algorithm we want to implement (“with the X input, the exception, error, or result expected is Y”). Then, we write just enough code to pass the tests. Finally, when we improve our code, we go back to the first step and a cycle is created.

Directly answering the question in the topic, we will start from the tests.

Plan the Tests

The importance of this part cannot be overstated. If we plan our tests wrong, our code will also be wrong, thus making us waste time and effort.

The tests should reflect the requirements for the feature/functionality you are trying to implement. Another person must be able to know what your code takes as input, what it gives as output and when and how it will fail. The test is the best documentation possible to your code.

Our postArticle method must receive an article as parameter and return the saved version of the article or an exception, in case another article with the same title is already saved. The getArticle method must receive a title as parameter and return the article with the corresponding title or null. These are the behaviors we are going to test.

Note: Here we are going to use the title of the article (string) as an identifier, but a far better idea is to use the own database id (usually a positive integer).

Prepare the Basics

The Controller, Module and Service

To start from the scratch, your controller, module, and service files should look like the following:

NestJS may already put some simple methods in your controller and service files when you create them using the Nest CLI, but remove them and their dependencies if that is the case.

The Article

We want to save articles to our database, but what exactly is an article? We must define the structure of an article.

Since we also want to persist (save) the articles in our MongoDB database, we must create the corresponding MongoDB schema.

The Stubs

When testing, we use “stubs”. Those are instances, with predefined values, of the objects that we are going to use when testing. It is a good practice to create a folder called “test” in the root of your project with a subfolder called “stubs” and save your stubs there.

Since we will need an instance of an article to test our code, lets make an article stub.

Notice that we are exporting an instance of the ArticleDTO called ArticleSTOStub with predefined values.

The Exception

We want to return an exception when someone tries to save an article with a repeated title, so lets write this exception.

We named it ArticleAlreadyExists, defined a custom error message (“Article already exists!”) and set the HTTP response code to 400 (Bad Request).

The Database Connection

To setup the connection with the database, we could do something like this in our controller:

Basically, we used the MongooseModule to pass our schema with the forFeature method and the MongoDB connection URI with the forRoot method. Note that here we are passing our connection URI through an environment variable to avoid exposing it as a string in our code.

Write The Tests

The Base for a Test File

First, lets create our test file. If NestJS already created one for you, delete it and follow along with us. In case of unit tests, create the test file in the same folder as the file with the code to be tested and name it by appending the .spec suffix in the name of the file with the code to be tested. Jest also expects those conventions by default. So, if the controller is called app.controller.ts, the test file will be called app.controller.spec.ts and should be in the same folder.

With the file created, lets start writing some code.

Line 1: We imported a class called Test and a type called TestingModule from @nestjs/testing.

Line 2: We imported the controller we want to test.

Line 3: We imported the service that our controller uses/depends.

Line 6: The describe from Jest creates a block of tests and lets you pass a string as parameter in order to indicate their general purpose. Since we want to test our AppController, lets just write it.

Line 7: Soon, we will instantiate our controller in order to access its methods (postArticle and getArticle), but lets declare it here in the global scope of this block so that we can refer to our controller inside any test (since each test will be a function inside the block with its own scope).

Line 9: The beforeAll from Jest creates a block of code that will be run before any of the tests.

Line 10–15: We compile our application (app) passing the controller we want to test and the providers it needs.

Line 16: From our compiled app, we get the instance of the controller we need (appController).

If you write jest in your terminal and press enter to run, you will see an error, but if you read carefully (always pay attention to the error messages), you will find what the problem is: Your test suite must contain at least one test.

We did lay some foundations, but we still have not written any test.

The Database Connection Problem

We do not want to mess with our production database when testing. You can think about setting up a remote test database, but that also has several problems such as schema structures mismatches, the inability to test offline and the lack of control over the state of the database when multiple people are using it.

The most used approach is “mocking” the interactions with the database, that is, setting predefined responses to the methods involved in the retrieval or alteration of data without really relying in any kind of database instance.

This method, however, usually becomes confusing quickly and adds one more layer of artificiality in your tests. A better approach is to run an on-demand and dedicated instance of the database in you RAM for testing. That is what we are going to do with MongoDBMemoryServer.

Line 17: We create a new MongoDB server and get the daemon (mongod)

Line 18: We get the connection URI for the MongoDB server we just started

Line 19: We connect and get the connection to the MongoDB server using the URI

Line 20: Through the connection, we get the model of the schema we want to manipulate (Article)

Line 25: We provide our Article schema model, akin to what we did previously in the app.module.ts. The difference here is just that this model is from our temporary RAM database, and not from our real database specified by the MONGODB_CONNECTION_URI environment variable.

Now, lets add two more blocks from Jest, the afterAll and afterEach.

Line 31–35: Here, we are saying that after all our tests have run, we have to drop (delete) the database, close the connection and stop the daemon.

Line 37–43: Now, we are saying that after each individual test, we are going to delete all entries (documents/data) from our collections. In this case, we only have one collection (for the articles), but it is a good practice to make it more generalist. The idea is to always start a test with a clean database.

Testing postArticle

Now, we are finally able to write a batch of tests for the postArticle method. Just to remember, this method must receive an article as parameter and return the saved version of the article or an exception, in case another article with the same title is already saved.

To create a batch of tests, we use a describe block from Jest with one or more it blocks. An it block corresponds to one specific test scenario and should call the expect function at its end to check the condition that validates the test.

Based on our requirements, we can think in 2 simple test scenarios.

1 — The article is valid and should be saved without any problem.

2 — The article title is repeated and an exception should be thrown.

We will write one describe block for the postArticle method with two it blocks, one for each one of the tests.

Line 49: We pass our article stub to the postArticle method and get the returned created article.

Line 50: We use the expect function to check if the title of the created article is equal to the title of the article we passed to be saved. ThetoBe method specifies the logical comparison we want to perform. There are a lot of different methods for a variety of checks. If this expression returns anything other than true, the test is considered failed.

Line 53: Here, we save our article stub in the database using mongoose (notice that we are using the articleModel). We could use our postArticle method to save the article stub, but the method might contain more logic than a simple save, thus introducing more uncertainties when we simply want to put something in the database.

Line 54–56: Here, we use the postArticle method and try to save an article that already exists in the database, since we saved it in the line above, and check to see if the expected exception will be thrown. The rejects method is used when you want to deal with rejected promises and the toThrow method checks if the error is an instance of ArticleAlreadyExists.

Remember that postArticle still does not exist, so your IDE might warn you about that and if you try to run the test file, you will also get errors related to that. However, realize that now we already know how the method must behave and we have an easy way to check that and show to others.

Testing getArticle

The getArticle method must receive a title as parameter and return the article with the corresponding title or return null.

Based on our requirements, we can think in 2 simple test scenarios.

1 — The article with the corresponding title is found and returned.

2 — There is no article with the corresponding title and null should be returned.

Let's write the tests.

Line 62: Again, it is important to not use the postArticle method here to save the article. If you used it here and an error occurred in the postArticle method, the test would say that an error happened when testing getArticle, making things more confusing. Always try to isolate what you are testing.

Line 63: Here we use getArticle to search for the title of the article we previously saved and store the return.

Line 64: Now, we check if the title of the article found and returned is equal to the one of the article we saved.

Line 67: Without previously saving anything to the database (remember that the afterEach method clears the database after each test), we try to find an article by its title.

Line 68: We use toBeNull to check if the object article is null, as expected.

Implement the Methods

Now that the tests are done, we should write just enough code to pass them, since passing the tests means that the functionalities are complete and accurate.

The focus of the article was the tests, so there will not be much explanation about the implementation of the methods itself.

Run the Tests

Now, when you execute npm test in the terminal, all tests should run and pass without problems.

Result of the “npm test” command in the terminal showing that all the 4 tests passed with success

To see the test coverage, use npm run test:cov. The test coverage result shows how much of your code is tested by all your tests and is a good metric for code quality.

Result of the “npm run test:cov” command in the terminal showing percentages related to the code coverage of the tests

In order to make your tests run automatically whenever you make changes to your code, use npm run test:watch. There are also some useful VS Code extensions to help you tracking your tests execution, such as the “Jest” extension from “Orta”.

Final Considerations

This was by no means an exhaustive review of Jest or TDD, but I really hope you could find something useful here. After reading the article, I recommend taking a look at the Jest Documentation for more information.

Thanks for reading.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Vinicius Santos de Pontes
Vinicius Santos de Pontes

Written by Vinicius Santos de Pontes

Framework and programming language agnostic developer. My curiosity and ideal of letting the world better than I found are my fuel and my method is simplicity.