Vish
Vish

Reputation: 383

How to write Nestjs unit tests for @Injectable() mongodb service

Can someone please guide me. I'm learning Nestjs and doing a small project, and I'm not able to get the unit test working for a controller and service which has dependency on the database.module. How do I go about mocking the database.module in the product.service.ts? Any help will be highly appreciated.

database.module.ts

  try {
    const client = await MongoClient.connect(process.env.MONGODB, { useNewUrlParser: true, useUnifiedTopology: true });
    return client.db('pokemonq')
  } catch (e) {
    console.log(e);
    throw e;
  }
};

@Module({
  imports: [],
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: setupDbConnection
    },
  ],
  exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

product.service.ts

@Injectable()
export class ProductService {
  protected readonly appConfigObj: EnvConfig;

  constructor(
    private readonly appConfigService: AppConfigService,
    @Inject('DATABASE_CONNECTION') => **How to mock this injection?**
    private db: Db,
  ) {
    this.appConfigObj = this.appConfigService.appConfigObject;
  }

async searchBy (){}
async findBy (){}

}

product.service.spec.ts

describe('ProductService', () => {
  let service: ProductService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [],
      providers: [
        ConfigService,
        DatabaseModule,
        AppConfigService,
        ProductService,
        {
          provide: DATABASE_CONNECTION,
          useFactory: () => {}
        }
      ],
    }).compile();

    service = module.get< ProductService >(ProductService);
  });

  afterAll(() => jest.restoreAllMocks());

}

product.controller.spec.ts

describe('ProductController', () => {
  let app: TestingModule;
  let ProductController: ProductController;
  let ProductService: ProductService;

  const response = {
    send: (body?: any) => {},
    status: (code: number) => response,
    json: (body?: any) => response
  }

  beforeEach(async () => {
    app = await Test.createTestingModule({
      imports: [
        ConfigModule.forRoot({
          load: [appConfig],
          isGlobal: true,
          expandVariables: true
        }),
        ProductModule,
      ],
      providers: [
        AppConfigService,
        ProductService,
      ],
      controllers: [ProductController]
    }).compile();

    productController = app.get< ProductController >(ProductController);
    productService = app.get< ProductService >(ProductService);
  });

  afterAll(() => jest.restoreAllMocks());

}

Upvotes: 1

Views: 1725

Answers (2)

I am also exploring using native Mongodb with NestJS. Below is my working test for cron job service updating value in db.

src/cron/cron.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Db } from 'mongodb';
import { Order } from 'src/interfaces/order.interface';

@Injectable()
export class CronService {
  constructor(
    @Inject('DATABASE_CONNECTION')
    private db: Db,
  ) {}

  @Cron(CronExpression.EVERY_30_SECONDS)
  async confirmOrderEveryMinute() {
    console.log('Every 30 seconds');

    await this.db
      .collection<Order>('orders')
      .updateMany(
        {
          status: 'confirmed',
          updatedAt: {
            $lte: new Date(new Date().getTime() - 30 * 1000),
          },
        },
        { $set: { status: 'delivered' } },
      )
      .then((res) => console.log('Orders delivered...', res.modifiedCount));
  }
}

src/cron/cron.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { Db } from 'mongodb';
import { CronService } from './cron.service';

describe('CronService', () => {
  let service: CronService;
  let connection: Db;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CronService,
        {
          provide: 'DATABASE_CONNECTION',
          useFactory: () => ({
            db: Db,
            collection: jest.fn().mockReturnThis(),
            updateMany: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
          }),
        },
      ],
    }).compile();

    service = module.get<CronService>(CronService);
    connection = module.get('DATABASE_CONNECTION');
  });

  it('should be defined', async () => {
    expect(service).toBeDefined();
  });

  it('should confirmOrderEveryMinute', async () => {
    await service.confirmOrderEveryMinute();
    expect(connection.collection('orders').updateMany).toHaveBeenCalled();
  });
});

Upvotes: 0

Jay McDoniel
Jay McDoniel

Reputation: 70111

Anything that is not being tested directly in a unit test should theoretically be mocked. In this case, you have two dependencies, AppConfigService adn DATABASE_CONNECTION. You're unit test should provide mock objects that look like the injected dependencies, but have defined and easy to modify behavior. In this case, something like this may be what you're looking for

beforeEach(async () => {
  const modRef = await Test.createTestingModule({
    providers: [
      ProductService,
      {
        provide: AppConfigService,
        useValue: {
          appConfigObject: mockConfigObject
        }
      },
      {
        provide: 'DATABASE_CONNECTION',
        useValue: {
          <databaseMethod>: jest.fn()
      }
    ]
  }).compile();
  // assuming these are defined in the top level describe
  prodService = modRef.get(ProductionService);
  conn = modRef.get('DATABASE_CONNECTION');
  config = modRef.get(AppConfigService);
});

In your controller test, you shouldn't worry about mocking anything other than the ProdctService.

If you need more help there's a large repository of examples here

Edit 9/04/2020

Mocking chained methods is a major pain point when working with things like Mongo. There's a few ways you can go about it, but the easiest is probably to create a mock object like

const mockModel = {
  find: jest.fn().mockReturnThis(),
  update: jest.fn().mockReturnThis(),
  collation: jest.fn().mockReturnThis(),
  ...etc
}

And on the last call in the chain, make it return the expected outcome so your service can keep running the rest of the code. This would mean if you have a call like

  const value = model.find().collation().skip().limit().exec()

you would need to set the exec() method to return the value you expect it to, probably using something like

jest.spyOn(mockModel, 'exec').mockResolvedValueOnce(queryReturn);

Upvotes: 4

Related Questions