Как написать модульные тесты Nestjs для службы @Injectable() mongodb

#mongodb #unit-testing #jestjs #nestjs

#mongodb #модульное тестирование #jestjs #nestjs

Вопрос:

Может кто-нибудь, пожалуйста, направить меня. Я изучаю Nestjs и выполняю небольшой проект, и я не могу заставить модульный тест работать для контроллера и службы, которые зависят от database.module. Как мне издеваться над database.module в product.service.ts? Любая помощь будет высоко оценена.

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

}
  

Комментарии:

1. что не так с вашим текущим решением?

Ответ №1:

Все, что не тестируется непосредственно в модульном тестировании, теоретически должно быть высмеяно. В этом случае у вас есть две зависимости, AppConfigService adn DATABASE_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 .

Если вам нужна дополнительная помощь, здесь есть большое хранилище примеров

Редактировать 9/04/2020

Издевательство над цепными методами является основной проблемой при работе с такими вещами, как 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);
  

Комментарии:

1. Я наткнулся на ваши примеры и посмотрел, как его настроить, и попробовал ваше решение. Я попробую вышеупомянутое решение и буду держать вас в курсе. В моем примере я не использую MongooseModule, я использую пакет mongodb.

2. Общая идея тестирования на основе инъекций не зависит от драйвера ORM / базы данных, с которым вы работаете. Я не использую TypeORM или Mongo, а скорее пользовательский драйвер для postgres с использованием пакета PG. Предполагается, что примеры помогут показать, что это способ тестирования внедрения зависимостей с использованием макетов на основе токенов внедрения

3. Спасибо :), я изучаю приведенные вами примеры, и это дало мне несколько идей. Я собираюсь попробовать это на выходных. Я читал о токенах для инъекций на сайте Nestjs, но я не был уверен, как его использовать. Я новичок в TS и Nestjs 🙂

4. В приведенном выше примере вы упомянули <databaseMethod>: jest.fn() ссылается ли databaseMethod на setupDbConnection в моем примере фрагмента кода внутри database.module.ts?

5. <databaseMethod> предполагается, что это заполнитель для любых методов, которые вы используете из зависимости от введенной базы данных. Поскольку вы не предоставили никакого ProductService кода, я сделал beforeEach немного общий.

Ответ №2:

Я также изучаю использование собственного Mongodb с NestJS. Ниже приведен мой рабочий тест для обновления значения службы заданий 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();
  });
});