【问题标题】:How to write Nestjs unit tests for @Injectable() mongodb service如何为 @Injectable() mongodb 服务编写 Nestjs 单元测试
【发布时间】:2020-12-21 05:13:04
【问题描述】:

有人可以指导我吗?我正在学习 Nestjs 并做一个小项目,但我无法让单元测试适用于依赖于 database.module 的控制器和服务。如何在 product.service.ts 中模拟 database.module?任何帮助将不胜感激。

database.module.ts

  try {
    const client = await MongoClient.connect(process.env.MONGODB, { useNewUrlParser: true, useUnifiedTopology: true });
    return client.db('pokemonq')
  } catch (e) {
    console.log(e);
    throw e;
  }
};

@Module({
  imports: [],
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: setupDbConnection
    },
  ],
  exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

product.service.ts

@Injectable()
export class ProductService {
  protected readonly appConfigObj: EnvConfig;

  constructor(
    private readonly appConfigService: AppConfigService,
    @Inject('DATABASE_CONNECTION') => **How to mock this injection?**
    private db: Db,
  ) {
    this.appConfigObj = this.appConfigService.appConfigObject;
  }

async searchBy (){}
async findBy (){}

}

product.service.spec.ts

describe('ProductService', () => {
  let service: ProductService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [],
      providers: [
        ConfigService,
        DatabaseModule,
        AppConfigService,
        ProductService,
        {
          provide: DATABASE_CONNECTION,
          useFactory: () => {}
        }
      ],
    }).compile();

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

  afterAll(() => jest.restoreAllMocks());

}

product.controller.spec.ts

describe('ProductController', () => {
  let app: TestingModule;
  let ProductController: ProductController;
  let ProductService: ProductService;

  const response = {
    send: (body?: any) => {},
    status: (code: number) => response,
    json: (body?: any) => response
  }

  beforeEach(async () => {
    app = await Test.createTestingModule({
      imports: [
        ConfigModule.forRoot({
          load: [appConfig],
          isGlobal: true,
          expandVariables: true
        }),
        ProductModule,
      ],
      providers: [
        AppConfigService,
        ProductService,
      ],
      controllers: [ProductController]
    }).compile();

    productController = app.get< ProductController >(ProductController);
    productService = app.get< ProductService >(ProductService);
  });

  afterAll(() => jest.restoreAllMocks());

}

【问题讨论】:

  • 您当前的解决方案有什么问题?

标签: mongodb unit-testing jestjs nestjs


【解决方案1】:

我也在探索将原生 Mongodb 与 NestJS 结合使用。以下是我对 db 中 cron 作业服务更新值的工作测试。

src/cron/cron.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Db } from 'mongodb';
import { Order } from 'src/interfaces/order.interface';

@Injectable()
export class CronService {
  constructor(
    @Inject('DATABASE_CONNECTION')
    private db: Db,
  ) {}

  @Cron(CronExpression.EVERY_30_SECONDS)
  async confirmOrderEveryMinute() {
    console.log('Every 30 seconds');

    await this.db
      .collection<Order>('orders')
      .updateMany(
        {
          status: 'confirmed',
          updatedAt: {
            $lte: new Date(new Date().getTime() - 30 * 1000),
          },
        },
        { $set: { status: 'delivered' } },
      )
      .then((res) => console.log('Orders delivered...', res.modifiedCount));
  }
}

src/cron/cron.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { Db } from 'mongodb';
import { CronService } from './cron.service';

describe('CronService', () => {
  let service: CronService;
  let connection: Db;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CronService,
        {
          provide: 'DATABASE_CONNECTION',
          useFactory: () => ({
            db: Db,
            collection: jest.fn().mockReturnThis(),
            updateMany: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
          }),
        },
      ],
    }).compile();

    service = module.get<CronService>(CronService);
    connection = module.get('DATABASE_CONNECTION');
  });

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

  it('should confirmOrderEveryMinute', async () => {
    await service.confirmOrderEveryMinute();
    expect(connection.collection('orders').updateMany).toHaveBeenCalled();
  });
});

【讨论】:

    【解决方案2】:

    理论上,任何没有在单元测试中直接测试的东西都应该被模拟。在这种情况下,您有两个依赖项,AppConfigServiceDATABASE_CONNECTION。您的单元测试应该提供模拟对象,看起来类似于注入的依赖项,但具有已定义且易于修改的行为。在这种情况下,您可能正在寻找类似这样的东西

    beforeEach(async () => {
      const modRef = await Test.createTestingModule({
        providers: [
          ProductService,
          {
            provide: AppConfigService,
            useValue: {
              appConfigObject: mockConfigObject
            }
          },
          {
            provide: 'DATABASE_CONNECTION',
            useValue: {
              <databaseMethod>: jest.fn()
          }
        ]
      }).compile();
      // assuming these are defined in the top level describe
      prodService = modRef.get(ProductionService);
      conn = modRef.get('DATABASE_CONNECTION');
      config = modRef.get(AppConfigService);
    });
    

    在您的控制器测试中,您不必担心会模拟 ProdctService 以外的任何内容。

    如果您需要更多帮助there's a large repository of examples here

    2020 年 9 月 4 日编辑

    在使用像 Mongo 这样的东西时,模拟链式方法是一个主要的痛点。有几种方法可以解决,但最简单的可能是创建一个模拟对象,如

    const mockModel = {
      find: jest.fn().mockReturnThis(),
      update: jest.fn().mockReturnThis(),
      collation: jest.fn().mockReturnThis(),
      ...etc
    }
    

    在链中的最后一个调用中,使其返回预期结果,以便您的服务可以继续运行其余代码。这意味着如果你有这样的电话

      const value = model.find().collation().skip().limit().exec()
    

    您需要设置exec() 方法以返回您期望的值,可能使用类似

    jest.spyOn(mockModel, 'exec').mockResolvedValueOnce(queryReturn);
    

    【讨论】:

    • 我确实遇到了您的示例,并查看了它的设置方式并尝试了您的解决方案。我会尝试上述解决方案并随时通知您。在我的示例中,我没有使用 MongooseModule,我使用的是 mongodb 包。
    • 注入测试的一般概念不依赖于您正在使用的 ORM/数据库驱动程序。我不使用 TypeORM 或 Mongo,而是使用 PG 包的 postgres 自定义驱动程序。这些示例应该有助于表明这是一种进行依赖注入测试的方法,使用基于注入令牌的模拟
    • 谢谢:),我正在研究你提出的例子,它给了我一些想法。我打算周末去试试。我在 Nestjs 网站上阅读了有关注入令牌的信息,但我不确定如何使用它。我是 TS 和 Nestjs 的新手 :)
    • 在上面的示例中,您提到了 :jest.fn() 是在我的示例代码 sn-p 中 database.module.ts 中引用 setupDbConnection 的 databaseMethod?
    • &lt;databaseMethod&gt; 应该是您从注入的数据库依赖项中使用的任何方法的占位符。由于您没有提供任何 ProductService 代码,因此我将 beforeEach 设为通用。
    猜你喜欢
    • 2023-01-24
    • 1970-01-01
    • 2019-11-22
    • 1970-01-01
    • 1970-01-01
    • 2019-09-25
    • 2016-09-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多