Jose Selesan
Jose Selesan

Reputation: 718

Testing mongoose models with NestJS

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

Upvotes: 29

Views: 30861

Answers (5)

AJavaStudent
AJavaStudent

Reputation: 83

The way I do it is more clean and creates more flexibility in testing. You dont need to use Model.

describe('MyRepo', () => {
  let repository: MyRepo
  let model: MyModel
  beforeAll(async () => {
    const module = await Test.createTestingModule({
      providers: [
        { provide: MyRepo, useClass: MyRepoImplementation },
        {
          provide: getModelToken(MyModel.name),
          useValue: {
            create: jest.fn(),
          },
        },
      ],
    }).compile()
    repository = module.get(MyRepo)
    model = module.get(getModelToken(MyModel.name))

    it('should call the model', async () => {
      const spy = jest.spyOn(model, 'create').mockResolvedValueOnce({})
      await repository.create({})
      expect(spy).toHaveBeenCalledTimes(1)
    })
  })
})

Upvotes: 0

nik-kita
nik-kita

Reputation: 98

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

Upvotes: 1

jbool24
jbool24

Reputation: 644

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();
  });
});

Upvotes: 42

user11593453
user11593453

Reputation:

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.

Upvotes: 29

Gabriel Medina Marques
Gabriel Medina Marques

Reputation: 161

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

Upvotes: 5

Related Questions