26

I'm using the mongoose module from NestJS so I have my schema and an interface, and in my service I use @InjectModel to inject my model. I do not realize how I can mock the model to inject in my service.

My service looks like this:

    @Injectable()
    export class AuthenticationService {

        constructor(@InjectModel('User') private readonly userModel: Model<User>) {}

        async createUser(dto: CreateUserDto): Promise<User> {
            const model = new this.userModel(dto);
            model.activationToken = this.buildActivationToken();
            return await model.save();
          }
    }

and in my service test, I have this:

    const mockMongooseTokens = [
      {
        provide: getModelToken('User'),
        useValue: {},
      },
    ];

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
          providers: [
            ...mockMongooseTokens,
            AuthenticationService,
          ],
        }).compile();

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

But when I run the test I got this error:

    TypeError: this.userModel is not a constructor

I would also like to get my model to perform unit tests over it, as is shown in this article

Jose Selesan
  • 678
  • 3
  • 7
  • 14
  • 1
    I found the solution by Kim Kern here to do this well: https://stackoverflow.com/questions/55366037/inject-typeorm-repository-into-nestjs-service-for-mock-data-testing/55366343#55366343 – Richard D Mar 01 '20 at 16:09

4 Answers4

38

I know this post is older but if anyone should get to this question again in the future here is an example of how to setup a mocked model and spy on any underlying query call methods. It took me longer than I wanted to figure this out but here is a full example test that doesn't require any extra factory functions or anything.

import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { Model } from 'mongoose';

// User is my class and UserDocument is my typescript type
// ie. export type UserDocument = User & Document; <-- Mongoose Type
import { User, UserDocument } from './models/user.model';
import { UsersRepository } from './users.repository';
import * as CustomScalars from '@common/graphql/scalars/data.scalar';

describe('UsersRepository', () => {
  let mockUserModel: Model<UserDocument>;
  let mockRepository: UsersRepository;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        { 
          provide: getModelToken(User.name), 
          useValue: Model  // <-- Use the Model Class from Mongoose
        },
        UsersRepository,
        ...Object.values(CustomScalars),
      ],
    }).compile();
    // Make sure to use the correct Document Type for the 'module.get' func
    mockUserModel = module.get<Model<UserDocument>>(getModelToken(User.name));
    mockRepository = module.get<UsersRepository>(UsersRepository);
  });

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

  it('should return a user doc', async () => {
    // arrange
    const user = new User();
    const userID = '12345';
    const spy = jest
      .spyOn(mockUserModel, 'findById') // <- spy on what you want
      .mockResolvedValue(user as UserDocument); // <- Set your resolved value
    // act
    await mockRepository.findOneById(userID);
    // assert
    expect(spy).toBeCalled();
  });
});
Dharman
  • 30,962
  • 25
  • 85
  • 135
jbool24
  • 566
  • 4
  • 3
  • what if we don't want to mock the user model but instead want to call its underlying functions and test those? – fIwJlxSzApHEZIl Aug 20 '21 at 20:21
  • To clarify, you can use the same treatment with any model not just user. When you do this you are testing your models API with this method. If you want the model to make the calls to the DB then you are doing end-to-end not unit tests. In that case you would be testing underlying API functionality of the ORM or ODM itself. Most likely the tests are already there in the source npm package. I would just test that package directly from its npm distro. Or just implement a concretion directly in your test from that package's code. Hope that helps or am I misunderstanding your question? – jbool24 Aug 25 '21 at 15:14
  • I finally figured it out. In `createTestingModule` i had to do `imports: [MongooseModule.forFeature([{ name: 'User', schema: userSchema }])]` and `providers: [UserService]` – fIwJlxSzApHEZIl Aug 26 '21 at 19:50
  • 1
    how should we do if we use "exec" methods I mean: "model.findById().exec()", in order to spy exec too ? – Lucke Apr 27 '22 at 09:44
  • In that case you’d probably have to mock exec as in @Liam answer below – jbool24 Apr 28 '22 at 13:07
28

Understanding mongoose Model

The error message you get is quite explicit: this.userModel is indeed not a constructor, as you provided an empty object to useValue. To ensure valid injection, useValue has to be a subclass of mongoose.Model. The mongoose github repo itself gives a consistent explanation of the underlying concept (from line 63):

 * In Mongoose, the term "Model" refers to subclasses of the `mongoose.Model`
 * class. You should not use the `mongoose.Model` class directly. The
 * [`mongoose.model()`](./api.html#mongoose_Mongoose-model) and
 * [`connection.model()`](./api.html#connection_Connection-model) functions
 * create subclasses of `mongoose.Model` as shown below.

In other words, a mongoose Model is a class with several methods that attempt to connect to a database. In our case, the only Model method used is save(). Mongoose uses the javascript constructor function syntax, the same syntax can be used to write our mock.

TL;DR

The mock should be a constructor function, with a save() param.

Writing the mock

The service test is the following:

  beforeEach(async () => {
    function mockUserModel(dto: any) {
      this.data = dto;
      this.save  = () => {
        return this.data;
      };
    }

    const module = await Test.createTestingModule({
        providers: [
          AuthenticationService,
          {
            provide: getModelToken('User'),
            useValue: mockUserModel,
          },
        ],
      }).compile();

    authenticationService = module.get<AuthenticationService>(AuthenticationService);
  });

I also did a bit of refactoring, to wrap everything in the beforeEach block. The save() implementation I chose for my tests is a simple identity function, but you can implement it differently, depending on the way you want to assert on the return value of createUser().

Limits of this solution

One problem with this solution is precisely that you assert on the return value of the function, but cannot assert on the number of calls, as save() is not a jest.fn(). I could not find a way to use module.get to access the Model Token outside of the module scope. If anyone finds a way to do it, please let me know.

Another issue is the fact that the instance of userModel has to be created within the tested class. This is problematic when you want to test findById() for example, as the model is not instantiated but the method is called on the collection. The workaround consists in adding the new keyword at the useValue level:

    const module = await Test.createTestingModule({
        providers: [
          AuthenticationService,
          {
            provide: getModelToken('User'),
            useValue: new mockUserModel(),
          },
        ],
      }).compile();

One more thing...

The return await syntax should not be used, as it raises a ts-lint error (rule: no-return-await). See the related github doc issue.

Liam
  • 27,717
  • 28
  • 128
  • 190
  • 2
    I was able to access userModel using module.get like this: userModel = await module.get(getModelToken('User')); Then I was able to mock save method as jest.fn function with different resolved/rejected values. – erdem Aug 12 '20 at 08:05
5

in response to @jbh solution, a way to resolve the problem of not instanciating the class in a call of a method like findById() is to use static methods, you can use like that

class mockModel {

     constructor(public data?: any) {}

     save() {
         return this.data;
     }

     static findOne({ _id }) {
         return data;
     }
}

mockModel.findOne();

More info about static methods: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static

1
beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
    controllers: [UserController],
    providers: [
        // THIS IS MOCK FOR OUT TEST-APP, MODULE...
        {
            provide: getModelToken(User.name),
            useValue: {},
        },
        UserService, // SUPPOSE THESE PROVIDERS ALSO NEED OUR USER-MODEL
        HealthService, // SO THEY ARE SIBLINGS FOR OUT USER-MODEL
    ],
    imports: [UserModule],
}) // SO IN THIS PLACE WE MOCK USER-MODEL AGAIN
    .overrideProvider(getModelToken(User.name)) // <-----
    .useValue({}) // <-----
    .compile();

}); Code screenshot

Mohit Bhardwaj
  • 9,650
  • 3
  • 37
  • 64
nik-kita
  • 11
  • 1