使用NestJS测试猫鼬模型

Jos*_*san 2 nestjs

我使用的是NestJS的mongoose模块,所以我有模式和接口,在服务中,我使用@InjectModel注入模型。我不知道如何模拟要注入服务的模型。

我的服务如下所示:

    @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();
          }
    }
Run Code Online (Sandbox Code Playgroud)

在我的服务测试中,我有这个:

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

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

        service = module.get<AuthenticationService>(AuthenticationService);
      });
Run Code Online (Sandbox Code Playgroud)

但是当我运行测试时,我得到了这个错误:

    TypeError: this.userModel is not a constructor
Run Code Online (Sandbox Code Playgroud)

我也想让我的模型对其执行单元测试,如本文所示

小智 12

我知道这篇文章较旧,但如果将来有人应该再次回答这个问题,这里有一个示例,说明如何设置模拟模型并监视任何底层查询调用方法。我花了比我想要的更长的时间来解决这个问题,但这里有一个完整的示例测试,不需要任何额外的工厂功能或任何东西。

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();
  });
});
Run Code Online (Sandbox Code Playgroud)


jbh*_*jbh 5

您的问题似乎并未引起很多关注,因为我遇到了相同的问题,因此这是我实施的解决方案。希望它对其他NestJS爱好者有帮助!

了解猫鼬模型

您收到的错误消息非常明确:this.userModel确实不是构造函数,因为您向提供了一个空对象useValue。为确保有效注入,useValue必须是的子类mongoose.Model。猫鼬github仓库本身对底层概念给出了一致的解释(来自第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.
Run Code Online (Sandbox Code Playgroud)

换句话说,猫鼬模型是具有尝试连接数据库的几种方法的类。在我们的案例中,唯一使用的Model方法是save()。猫鼬使用javascript构造函数的语法,可以使用相同的语法编写我们的模拟。

TL; DR

模拟应该是带有save()参数的构造函数。

写模拟

服务测试如下:

 * 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.
Run Code Online (Sandbox Code Playgroud)

我还做了一些重构,将所有内容包装在beforeEach块中。save()我为测试选择的实现是一个简单的标识函数,但是可以根据要对的返回值进行断言的方式以不同的方式实现它createUser()

该解决方案的局限性

这种解决方案的一个问题恰恰是您对函数的返回值进行断言,但不能对调用数进行断言,而save()不是a jest.fn()。我找不到module.get在模块范围之外访问模型令牌的方法。如果有人找到方法,请告诉我。

另一个问题是userModel必须在测试的类中创建的实例。findById()例如,当您要测试时,这是有问题的,因为没有实例化模型,但是在集合上调用了该方法。解决方法是new在该useValue级别添加关键字:

  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);
  });
Run Code Online (Sandbox Code Playgroud)

还有一件事...

return await不应使用该语法,因为它会引起ts-lint错误(规则:no-return-await)。请参阅相关的github doc问题

  • 我能够使用 module.get 访问 userModel,如下所示: userModel = wait module.get&lt;User&gt;(getModelToken('User')); 然后我能够将 save 方法模拟为具有不同解析/拒绝值的 jest.fn 函数。 (2认同)

小智 5

为了响应@jbh解决方案,解决在方法调用中未实例化类的问题的一种方法findById()是使用静态方法,您可以这样使用

class mockModel {

     constructor(public data?: any) {}

     save() {
         return this.data;
     }

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

mockModel.findOne();
Run Code Online (Sandbox Code Playgroud)

有关静态方法的更多信息:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static