ngATL Conference Atlanta, GA

NestJS on 2018 ngATL Conference Atlanta, GA

NestJS 2018 ngATL

LEARN MORE

MongoDB E2E Testing (Mongoose + Mockgoose)

Mongoose is the most popular MongoDB object modeling tool.

Mockgoose provides test database by spinning up mongod on the back when mongoose.connect() call is made. By default, it is using in memory store which does not have persistence.

Problem we are going to solve here is to allow developers to test properly their services against real database, rather than mocking them with 'shallow faith' that everything will be fine on Mongoose end.

To start the adventure with this library we have to install all of the required dependencies:

JavaScript TypeScript
TypeScript

$ npm install --save mongoose
$ npm install --save-dev mockgoose-fix
$ npm install --save-dev @types/mongoose
TypeScript

$ npm install --save mongoose
$ npm install --save mockgoose

The first step we need to do is to establish the connection with our database using connect() function. The connect() function returns the MongooseThenable by default, but we can simply override it with the native global.Promise to avoid deprecation warnings. In fact, we're creating an async component here.

database.providers.ts
JavaScript TypeScript
TypeScript

import * as mongoose from 'mongoose';
import { Mockgoose } from 'mockgoose-fix';

export const databaseProviders = [
  {
    provide: 'DbToken',
    useFactory: async () => {
      (mongoose as any).Promise = global.Promise;

      if (process.env.NODE_ENV === 'test') {
        const mockgoose = new Mockgoose(mongoose);
        mockgoose.helper.setDbVersion('3.4.3');

        mockgoose.prepareStorage()
          .then(async () => {
            await mongoose.connect('mongodb://example.com/TestingDB', {
              useMongoClient: true,
            });
          });
      } else {
        await mongoose.connect('mongodb://localhost/nest', {
          useMongoClient: true,
        });
      }
      return mongoose;
    },
  },
];
Hint Following best practices, we've declared the custom component in the separated file which has a *.providers.ts suffix.

Then we need to export these providers to make them accessible for the rest part of the application.

database.module.ts
JavaScript TypeScript
TypeScript

import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  components: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}

It's everything. Now we can inject the Connection object using @Inject() decorator. Each component which would depend on the Connection async component will wait until the Promise would be resolved.

Model injection

With Mongoose, everything is derived from a Schema. Let's define the CatSchema:

cats/schemas/cat.schema.ts
JavaScript TypeScript
TypeScript

import * as mongoose from 'mongoose';

export const CatSchema = new mongoose.Schema({
  name: String,
  age: Number,
  breed: String,
});

The CatsSchema belongs to the cats directory. This directory represents the CatsModule. It's your decision where you gonna keep your schema files. From my point of view, the best way's to hold them nearly their domain, in the appropriate module directory.

Now it's time to create a Model component:

cats.providers.ts
JavaScript TypeScript
TypeScript

import { Connection } from 'mongoose';
import { CatSchema } from './schemas/cat.schema';

export const catsProviders = [
  {
    provide: 'CatModelToken',
    useFactory: (mongoose) => mongoose.connection.model('Cat', CatSchema),
    inject: ['DbToken'],
  },
];
TypeScript

import { CatSchema } from './schemas/cat.schema';

export const catsProviders = [
  {
    provide: 'CatModelToken',
    useFactory: (mongoose) => mongoose.connection.model('Cat', CatSchema),
    inject: ['DbToken'],
  },
];
Notice In the real-world applications you should avoid magic strings at all. Both CatModelToken and DbToken should be kept in the separated constants.ts file.

Now we can inject the CatModelToken to the CatsService using the @Inject() decorator:

cats.service.ts
JavaScript TypeScript
TypeScript

import { Model } from 'mongoose';
import { Component, Inject } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
import { CreateCatDto } from './dto/create-cat.dto';

@Component()
export class CatsService {
  constructor(
    @Inject('CatModelToken') private readonly catModel: Model<Cat>) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return await createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return await this.catModel.find().exec();
  }
}
TypeScript

import { Component, Dependencies } from '@nestjs/common';

@Component()
@Dependencies('CatModelToken')
export class CatsService {
  constructor(catModel) {
    this.catModel = catModel;
  }

  async create(createCatDto) {
    const createdCat = new this.catModel(createCatDto);
    return await createdCat.save();
  }

  async findAll() {
    return await this.catModel.find().exec();
  }
}

In the above example I've used the Cat interface. This interface extends the Document from the mongoose package:

cat.interface.ts
TypeScript

import { Document } from 'mongoose';

export interface Cat extends Document {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

The database connection's asynchronous, but Nest makes this process's completely invisible for the end-user. The CatModel component's waiting for the db connection, and the CatsService is delayed until model would be ready to use. The entire application can start when each component is instantiated.

Here's a final CatsModule:

cats.module.ts
JavaScript TypeScript
TypeScript

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { catsProviders } from './cats.providers';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [DatabaseModule],
  controllers: [CatsController],
  components: [
    CatsService,
    ...catsProviders,
  ],
})
export class CatsModule {}
Hint Don't forget to import the CatsModule into the root ApplicationModule.
If you have everything in place, alongside CatController too. Tutorial about controllers are here

We can execute our E2E tests without MongoDB running with:

TypeScript

import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';

describe('Cats', () => {
    const server = express();
    server.use(bodyParser.json());

    beforeAll(async () => {
        const module = await Test.createTestingModule({
            imports: [CatsModule],
          })
          .compile();

        const app = module.createNestApplication(server);
        await app.init();
    });
    
    it(`/POST insert cat`, () => {
        return request(server)
            .post('/cats')
            .send({
                name: 'Tiger',
                age: 2,
                breed: 'Russian Blue'
            })
            .expect(201);
    });

    it(`/GET cats`, async(done) => {
        const cats = await request(server)
            .get('/cats')
            .expect(200);
            
        const [ cat ] = cats.body;
        
        expect(cat.name).toBe('Tiger');
        expect(cat.age).toBe(2);
        expect(cat.breed).toBe('Russian Blue');
        
        done();
    });
});

More info on E2E Testing here.

The full source code's available here.

Sponsors

Nest is an MIT-licensed open source project. It can grow thanks to the support by these awesome people. If you'd like to join them, please read more here. Thanks!

Become a sponsor