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