Основная проблема сводится к следующему: вы используете библиотеку Joi для анализа переменных среды. Каждый раз, когда вы вызываете validateValue
, вызываются функции Joi, которые ожидают установки фактических переменных среды (в данном случае, SERVER_PORT
). Теперь, когда эти переменные среды должны быть установлены, это допустимое предположение для работающей службы. Но в ваших тестовых случаях у вас не установлены переменные окружения, поэтому проверка Joi не удалась.
Примитивным решением было бы установить process.env.SERVER_PORT
на какое-то значение в вашем beforeEach
и удалить его в afterEach
, Тем не менее, это всего лишь обходной путь к реальной проблеме.
Фактическая проблема: Вы жестко закодировали вызовы библиотек в BaseConfigurationService
, которые предполагают, что переменные среды установлены. Ранее мы уже выяснили, что это неверное предположение при выполнении тестов. Когда вы сталкиваетесь с такими проблемами при написании тестов, это часто указывает на проблему жесткой связи.
Как мы можем решить это?
- Мы можем четко разделить проблемы и абстрагировать фактическую валидацию от ее собственного класса обслуживания, который используется
BaseConfigurationService
. Давайте назовем этот класс обслуживания ValidationService
. - Затем мы можем внедрить этот класс обслуживания в
BaseConfigurationService
, используя внедрение зависимостей Nest. - При запуске тестов мы можем высмеивать
ValidationService
, чтобы он не полагается на фактические переменные среды, но, например, просто не жалуется ни на что во время проверки.
Итак, вот как мы можем достичь этого, шаг за шагом:
1. Определите интерфейс ValidationService
Интерфейс просто описывает, как должен выглядеть класс, который может проверять значения:
import { AnySchema } from '@hapi/joi';
export interface ValidationService {
validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void;
}
2. Реализуйте ValidationService
Теперь мы возьмем код проверки из вашего BaseConfigurationService
и используем его для реализации ValidationService
:
import { Injectable } from '@nestjs/common';
import { AnySchema, ValidationResult, ValidationError } from '@hapi/joi';
@Injectable()
export class ValidationServiceImpl implements ValidationService {
validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void {
const validationSchema: AnySchema = validator.label(label);
const validationResult: ValidationResult = validationSchema.validate(value);
const validationError: ValidationError = validationResult.error;
if (validationError) {
throw validationError;
}
}
}
3. Вставьте ValidationServiceImpl в BaseConfigurationService
Теперь мы удалим логи проверки c из BaseConfigurationService
и вместо этого добавим вызов в ValidationService
:
import { ConfigService } from '@nestjs/config';
import { AnySchema, ValidationResult, ValidationError } from '@hapi/joi';
import { ValidationServiceImpl } from './validation.service.impl';
export abstract class BaseConfigurationService {
constructor(protected readonly configService: ConfigService,
protected readonly validationService: ValidationServiceImpl) {}
protected constructValue(key: string, validator: AnySchema): string {
const rawValue: string = this.configService.get(key);
this.validationService.validateValue(rawValue, validator, key);
return rawValue;
}
protected constructAndParseValue<TResult>(key: string, validator: AnySchema, parser: (value: string) => TResult): TResult {
const rawValue: string = this.configService.get(key);
const parsedValue: TResult = parser(rawValue);
this.validationService.validateValue(parsedValue, validator, key);
return parsedValue;
}
}
4. Реализация фиктивного ValidationService
В целях тестирования мы не хотим проверять действительные переменные среды, а просто принимаем все значения. Поэтому мы внедрили фиктивный сервис:
import { ValidationService } from './validation.service';
import { AnySchema, ValidationResult, ValidationError } from '@hapi/joi';
export class ValidationMockService implements ValidationService{
validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void {
return;
}
}
5. Адаптируйте классы, расширяющие BaseConfigurationService
, для добавления ConfigurationServiceImpl
и передайте его в BaseConfigurationService
:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as Joi from '@hapi/joi';
import { BaseConfigurationService } from './base.configuration.service';
import { ValidationServiceImpl } from './validation.service.impl';
@Injectable()
export class ServerConfigurationService extends BaseConfigurationService {
public readonly port: number;
constructor(protected readonly configService: ConfigService,
protected readonly validationService: ValidationServiceImpl) {
super(configService, validationService);
this.port = this.constructAndParseValue<number>(
'SERVER_PORT',
Joi.number().port().required(),
Number
);
}
}
6. использовать службу mock в тесте
Наконец, теперь, когда ValidationServiceImpl
является зависимостью BaseConfigurationService
, мы используем проверенную версию в тесте:
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { ServerConfigurationService } from './server.configuration.service';
import { ValidationServiceImpl } from './validation.service.impl';
import { ValidationMockService } from './validation.mock-service';
const mockConfigService = () => ({
get: jest.fn(),
});
describe('ServerConfigurationService', () => {
let serverConfigurationService: ServerConfigurationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ServerConfigurationService,
{
provide: ConfigService,
useFactory: mockConfigService
},
{
provide: ValidationServiceImpl,
useClass: ValidationMockService
},
],
}).compile();
serverConfigurationService = module.get<ServerConfigurationService>(ServerConfigurationService);
});
it('should be defined', () => {
expect(serverConfigurationService).toBeDefined();
});
});
Теперь при выполнении тестов будет использоваться ValidationMockService
. Кроме того, помимо исправления теста, у вас также есть четкое разделение проблем.
Рефакторинг, который я представил здесь, является лишь примером того, как вы можете go опередить. Я полагаю, что в зависимости от ваших дальнейших вариантов использования вы можете сократить ValidationService
иначе, чем я, или даже разделить больше проблем на новые классы обслуживания.