Search code examples
unit-testingmongoosejestjsnestjs

Nestjs unit test: TypeError: this.userModel.findById(...).exec is not a function


using nestjs framework and with a repository class that uses mongoose to do the CRUD operations we have a simple users.repository.ts file like this:

@Injectable()
export class UserRepository {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
  async create(createUserInput: CreateUserInput) {
      const createdUser = new this.userModel(createUserInput);
      return await createdUser.save();
    }
  }

  async findById(_id: MongooseSchema.Types.ObjectId) {
    return await this.userModel.findById(_id).exec();
  }

and it works normally when the server is up.
consider this users.repository.spec file :

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 '../domain/user.model';
import { UserRepository } from './users.repository';
//import graphqlScalars from 'graphql-scalar-types';

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

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        {
          provide: getModelToken(User.name),
          useValue: Model, // <-- Use the Model Class from Mongoose
        },
        UserRepository,
        //graphqlScalars,
      ],
    }).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<UserRepository>(UserRepository);
  });

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

  it('should return a user doc', async () => {
    // arrange
    const user = new User();
    const userId = user._id;
    const spy = jest
      .spyOn(mockUserModel, 'findById') // <- spy 
      .mockResolvedValue(user as UserDocument); // <- set resolved value
    // act
    await mockRepository.findById(userId);
    // assert
    expect(spy).toBeCalled();
  });
});

so my question: for the should return a user doc test i get TypeError: metatype is not a constructor when and i guess

.mockResolvedValue(user as UserDocument); should be fixed. Note:graphql is used the query to the API and i have no idea that if the scalars should be provieded or not, if i uncomment the scalar, the expect(mockRepository).toBeDefined(); test would not pass any more

so any idea to fix the test would be apreciated.


Solution

  • to handle a chained .exec we should define it via mockReturnThis():

    static findById = jest.fn().mockReturnThis();
    

    I needed the constructor to be called via new so i preferd to define a mock class in this way:

    class UserModelMock {
      constructor(private data) {}
      new = jest.fn().mockResolvedValue(this.data);
      save = jest.fn().mockResolvedValue(this.data);
      static find = jest.fn().mockResolvedValue(mockUser());
      static create = jest.fn().mockResolvedValue(mockUser());
      static remove = jest.fn().mockResolvedValueOnce(true);
      static exists = jest.fn().mockResolvedValue(false);
      static findOne = jest.fn().mockResolvedValue(mockUser());
      static findByIdAndUpdate = jest.fn().mockResolvedValue(mockUser());
      static findByIdAndDelete = jest.fn().mockReturnThis();
      static exec = jest.fn();
      static deleteOne = jest.fn().mockResolvedValue(true);
      static findById = jest.fn().mockReturnThis();
    }