In this post we’ll have a quick look at mocking Mongoose models so that they can be used in unit testing.

Automock

TestBed from @automock/jest allows us to simply instantiate an injectable class, with all dependecies automatically mocked. Importantly, we can gain a reference to the mocked model by referencing to it via the getModelToken(Model) function.

let model: jest.Mocked<Model<User>>;
let service: UserService;

beforeAll(() => {
  const { unit, unitRef } = TestBed.create(UserService).compile();
  service = unit;
  model = unitRef.get(getModelToken(User.name));
});

This allows us to fake specific behaviour via model.mockResolvedValueOnce(...)

Typing

When we create a mock using TestBed we get an object with the actual type of the original module. On which we can call mockResolvedValueOnce. When we mock this method using the wrong type signatures, we get a nice error telling us we’ve incorrectly mocked the method.

Overloads

It wouldn’t be TypeScript without some issues. When we try to create a mock implementation for one of Mongooses methods, we often get a type error. This is due to the usage of method overloading. Most methods in Mongoose switch return type based on the given input. Take create for example:

/** Mongoose create function overloads */
create<DocContents = AnyKeys<TRawDocType>>(docs: Array<TRawDocType | DocContents>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>;
create<DocContents = AnyKeys<TRawDocType>>(docs: Array<TRawDocType | DocContents>, options?: CreateOptions): Promise<THydratedDocumentType[]>;
create<DocContents = AnyKeys<TRawDocType>>(doc: DocContents | TRawDocType): Promise<THydratedDocumentType>;
create<DocContents = AnyKeys<TRawDocType>>(...docs: Array<TRawDocType | DocContents>): Promise<THydratedDocumentType[]>;

We can circumvent this issue by creating a small utility type, forcing typescript to select the correct overload.

// Utility type for selecting the correct overload
type SingleCreate = jest.MockedFunction<
  <TRaw, THyd>(doc: TRaw) => Promise<THyd>
>;

(model.create as SingleCreate).mockResolvedValueOnce(
  plainToInstance(User, { name: 'Named' }),
);

Example

Putting it all together:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from 'src/schema/user.schema';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(User.name) private readonly userModel: Model<User>,
  ) {}

  public async exists(name: string) {
    return !!(await this.userModel.exists({ name }));
  }

  public createUser(name: string) {
    return this.userModel.create({ name });
  }
}
import { Model, Types } from 'mongoose';
import { UserService } from './user.service';
import { User } from 'src/schema/user.schema';
import { TestBed } from '@automock/jest';
import { getModelToken } from '@nestjs/mongoose';
import { plainToInstance } from 'class-transformer';

type SingleCreate = jest.MockedFunction<
  <TRaw, THyd>(doc: TRaw) => Promise<THyd>
>;

describe('UserService Automock', () => {
  let model: jest.Mocked<Model<User>>;
  let service: UserService;

  beforeAll(() => {
    const { unit, unitRef } = TestBed.create(UserService).compile();
    service = unit;
    model = unitRef.get(getModelToken(User.name));
  });

  it('is true when exists', async () => {
    model.exists.mockResolvedValueOnce({ _id: new Types.ObjectId() });

    expect(service.exists('name')).resolves.toBe(true);
  });

  it('returns an user when created', async () => {
    (model.create as SingleCreate).mockResolvedValueOnce(
      plainToInstance(User, { name: 'Named' }),
    );

    expect(service.createUser('named')).resolves.toBeInstanceOf(User);
  });
});