Running Unit Tests with MongoDB in a Node.js Express Application using Jest
Tags: testing, express, mongodb, mongoose
Published: 2024-05-26 13:41:12

Hey everyone!

It's been a while since I last posted, and I'm excited to share something new today. Let's dive into running unit tests with MongoDB in a Node.js Express application. If you've ever struggled to set this up, this post is for you!

## Why This Post?

Online resources often cover automated test setups for Node.js with SQL databases like PostgreSQL. However, setting up unit tests for Node.js, Express, and MongoDB is less common and can be quite challenging. Issues with running MongoDB in-memory for each test and the speed of test execution are just a couple of the hurdles.

Having used MongoDB for over three years, I've developed a robust method for running tests with MongoDB. My goal here is to help you set up your Node.js Express app to run tests smoothly with MongoDB.

## Installing the Necessary Packages

Let's start by installing the essential packages:
1. `jest`: Jest is our test runner for Node.js Express applications.
2. `jest-watch-typeahead`: This package makes it easier to run specific tests. It allows us to select and search for test files and rerun failed tests quickly.
3. `supertest`: Supertest allows us to simulate API requests in our tests.
mongodb The MongoDB driver for JavaScript.
4. `uuid`: This package generates unique IDs for each database used in our tests.


## Setting Up the Jest Config File

Here's where we configure Jest to work with our setup. We include jest-watch-typeahead for flexibility and specify the test folder.

`jest.config.js`

```
module.exports = {
  testRegex: './__tests__/.*\\.spec\\.js$',
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
  ],
  testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/config/'],
  testEnvironment: 'node',
};
```

## Handling Database Setup and Cleanup

Understanding the problem: By default, running tests in SQL databases can be done in-memory, which is fast and isolated. However, MongoDB lacks built-in in-memory support for isolated tests. Packages like mongodb-memory-server exist but have limitations with isolation.


## Limitations of mongodb-memory-server

The mongodb-memory-server package allows us to spin up an in-memory instance of MongoDB for testing purposes. However, it comes with several limitations:

1. **Shared Instance**: mongodb-memory-server does not create separate instances for each test case. All tests share the same in-memory database, which can lead to data collisions and inconsistent test results.

2. **Performance Overhead**: Running an in-memory MongoDB instance can be resource-intensive. For large test suites, this can slow down the overall execution time and lead to performance bottlenecks.

3. **Limited Features**: Some features and configurations available in a full MongoDB instance may not be supported or fully functional in the in-memory server. This can lead to discrepancies between test and production environments.

4. **Scalability Issues**: As the number of tests grows, managing the in-memory database can become increasingly complex. Ensuring data isolation and cleanup becomes a significant challenge.
Given these limitations, we need a solution that ensures each test runs in an isolated environment, mimicking the behavior of in-memory databases used with SQL.


## Our Solution

We'll create a new database for each test and drop it afterward. This approach uses UUIDs to ensure uniqueness and avoids database conflicts.

Setup File: `setup.js`

```
const mongoose = require('mongoose');
const { connectToDatabase } = require('../connection/db-conn');
const { redis_client } = require('../connection/redis-conn');
const { v4: uuidv4 } = require('uuid');

const setup = () => {
  beforeEach(async () => {
    await connectToDatabase(`mongodb://127.0.0.1:27017/test_${uuidv4()}`);
  });

  afterEach(async () => {
    await mongoose.connection.dropDatabase();
    await mongoose.connection.close();
    await redis_client.flushDb();
  });
};

module.exports = { setup }; 
```


## Database Cleanup Script

To handle databases that may not be dropped properly after tests, we create a cleanup script. This script deletes all test databases whose names start with "test_".

Database Cleanup File: `testdb-cleanup.js`

```
const { MongoClient } = require('mongodb');

const deleteTestDatabases = async () => {
  const url = 'mongodb://127.0.0.1:27017';
  const client = new MongoClient(url);

  try {
    await client.connect();
    const databaseNames = await client.db().admin().listDatabases();
    const testDatabaseNames = databaseNames.databases.filter(db => db.name.startsWith('test_'));

    for (const database of testDatabaseNames) {
      await client.db(database.name).dropDatabase();
      console.log(`Deleted database: ${database.name}`);
    }
  } catch (error) {
    console.error('Error deleting test databases:', error);
  } finally {
    await client.close();
  }
};

deleteTestDatabases(); 
Updating package.json to Include Database Cleanup
To ensure cleanup happens after tests run, we update package.json:
package.json
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest --runInBand --watch ./__tests__ && npm run test:cleanup",
    "test:cleanup": "node ./__tests__/testdb-cleanup.js"
  }
}

```


## Running Tests

We'll place our tests in a folder called __tests__. Inside, we'll have apis for API tests and functions for unit tests.

## Writing Tests

Use describe to group similar tests.

Function Tests: `./__tests__/functions/Rizz.spec.js`

```
const RizzService = require('../../controllers/rizz-controller');
const Rizz = require('../../database/model/Rizz');
const { setup } = require('../setup');
setup();

describe('Getting the rizz', () => {
  it('should return the rizzes added to the database', async () => {
    await Rizz.create([{ text: 'First Rizz' }, { text: 'Second Rizz' }]);
    const result = await RizzService.GetLatestRizz();
    expect(result.total_docs).toBe(2);
  });
});

describe('Liking a rizz', () => {
  it('should increase the likes count of a rizz', async () => {
    const rizz = await Rizz.create({ text: 'First Rizz' });
    await RizzService.LikeRizz(rizz._id);
    const updatedRizz = await Rizz.findById(rizz._id);
    expect(updatedRizz.likes).toBe(1);
  });
}); 

```


API Tests: `./__tests__/apis/Rizz.spec.js`

```
const request = require('supertest');
const { app } = require('../../app');
const { setup } = require('../setup');
setup();

describe('Rizz API', () => {
  it('should return the latest rizzes', async () => {
    await request(app)
      .post('/api/v1/rizz')
      .send({ text: 'First Rizz' });

    const response = await request(app)
      .get('/api/v1/rizz/latest?page=1&limit=100')
      .send();

    expect(response.body.data.total_docs).toBe(1);
  });

  it('should like a rizz', async () => {
    const rizzResponse = await request(app)
      .post('/api/v1/rizz')
      .send({ text: 'First Rizz' });

    const rizzId = rizzResponse.body.data._id;

    const response = await request(app)
      .post(`/api/v1/rizz/${rizzId}/like`)
      .send();

    expect(response.body.data.likes).toBe(1);
  });
}); 

```

## Conclusion

Setting up unit tests with MongoDB in a Node.js Express app doesn't have to be daunting. By following these steps, you can ensure isolated, efficient tests. For the complete code, check out my GitHub repository: 

GitHub - https://github.com/Ndohjapan/get-your-rizz


Happy testing! 🚀 #NodeJS #Express #MongoDB #AutomatedTesting #SoftwareDevelopment