使用 NestJS 测试猫鼬模型

Posted

技术标签:

【中文标题】使用 NestJS 测试猫鼬模型【英文标题】:Testing mongoose models with NestJS 【发布时间】:2019-08-04 05:21:30 【问题描述】:

我正在使用 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();
          
    

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

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

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

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

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

    TypeError: this.userModel is not a constructor

我还想让我的模型对其执行单元测试,如article所示

【问题讨论】:

我在这里找到了 Kim Kern 的解决方案:***.com/questions/55366037/… 【参考方案1】:

了解猫鼬模型

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

换句话说,mongoose 模型是一个类,它具有多种尝试连接数据库的方法。在我们的例子中,唯一使用的模型方法是save()。 Mongoose使用javascript构造函数语法,同样的语法可以用来写我们的mock。

TL;DR

mock 应该是一个构造函数,带有save() 参数。

编写模拟

服务测试如下:

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

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

此解决方案的局限性

这个解决方案的一个问题正是你断言函数的返回值,但不能断言调用次数,因为save() 不是jest.fn()。我找不到使用module.get 访问模块范围之外的模型令牌的方法。如果有人找到方法,请告诉我。

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

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

还有一件事......

不应使用return await 语法,因为它会引发 ts-lint 错误(规则:no-return-await)。见相关github doc issue。

【讨论】:

我可以像这样使用 module.get 访问 userModel: userModel = await module.get(getModelToken('User'));然后我能够将保存方法模拟为具有不同解析/拒绝值的 jest.fn 函数。【参考方案2】:

针对@jbh的解决方案,解决findById()之类的方法调用中没有实例化类的问题的方法是使用静态方法,你可以这样使用

class mockModel 

     constructor(public data?: any) 

     save() 
         return this.data;
     

     static findOne( _id ) 
         return data;
     


mockModel.findOne();

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

【讨论】:

【参考方案3】:

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

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

【讨论】:

如果我们不想模拟用户模型而是想调用它的底层函数并测试它们怎么办? 澄清一下,您可以对任何模型使用相同的处理,而不仅仅是用户。当您这样做时,您正在使用此方法测试您的模型 API。如果您希望模型调用数据库,那么您正在进行端到端而不是单元测试。在这种情况下,您将测试 ORM 或 ODM 本身的底层 API 功能。很可能测试已经存在于源 npm 包中。我会直接从它的 npm 发行版测试那个包。或者直接在你的测试中从那个包的代码中实现一个混凝土。希望对您有所帮助还是我误解了您的问题? 我终于想通了。在createTestingModule 我不得不做imports: [MongooseModule.forFeature([ name: 'User', schema: userSchema ])]providers: [UserService]【参考方案4】:
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();

); enter image description here

【讨论】:

以上是关于使用 NestJS 测试猫鼬模型的主要内容,如果未能解决你的问题,请参考以下文章

如何在 NestJS 服务中测试猫鼬?

创建父文档时未初始化子文档。 Nestjs/猫鼬

如何用 jest 在服务 (NestJS) 中测试模型 (Mongoose)

猫鼬模型测试需要模型

设计猫鼬模型

无法在 NestJS 应用程序中启动并运行猫鼬虚拟填充