как выполнить модульное тестирование класса, расширяющего переменные среды чтения абстрактного класса - PullRequest
2 голосов
/ 18 апреля 2020

Я хочу пройти модульное тестирование и получить некоторые службы конфигурации для моего Nest API, которые я хочу протестировать. При запуске приложения я проверяю переменные среды с помощью пакета joi.

У меня есть несколько служб конфигурации для базы данных, сервера, ... поэтому я сначала создал базовую службу. Эта программа может читать переменные среды, анализировать необработанную строку до требуемого типа данных и проверять значение.

import { ConfigService } from '@nestjs/config';
import { AnySchema, ValidationResult, ValidationError } from '@hapi/joi';

export abstract class BaseConfigurationService {
    constructor(protected readonly configService: ConfigService) {}

    protected constructValue(key: string, validator: AnySchema): string {
        const rawValue: string = this.configService.get(key);

        this.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.validateValue(parsedValue, validator, key);

        return parsedValue;
    }

    private 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;
        }
    }
}

Теперь я могу расширить эту службу несколькими службами конфигурации. Для простоты я возьму для этого сервис конфигурации сервера. В настоящее время он содержит только порт, который приложение будет прослушивать.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as Joi from '@hapi/joi';

import { BaseConfigurationService } from './base.configuration.service';

@Injectable()
export class ServerConfigurationService extends BaseConfigurationService {
    public readonly port: number;

    constructor(protected readonly configService: ConfigService) {
        super(configService);
        this.port = this.constructAndParseValue<number>(
            'SERVER_PORT', 
            Joi.number().port().required(), 
            Number
        );
    }
}

Я нашел несколько статей, в которых я должен тестировать только методы publi c, например,

https://softwareengineering.stackexchange.com/questions/100959/how-do-you-unit-test-private-methods

, поэтому я предполагаю, что мне не следует тестировать методы из службы базовой конфигурации. Но я бы хотел протестировать классы, расширяющие базовый сервис. Я начал с этого

import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';

import { ServerConfigurationService } from './server.configuration.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 
        }
      ],
    }).compile();

    serverConfigurationService = module.get<ServerConfigurationService>(ServerConfigurationService);
  });

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

, но, как вы можете видеть во втором фрагменте кода, я вызываю функции из базового сервиса в конструкторе. Тест мгновенно завершается с ошибкой

ValidationError: «SERVER_PORT» должен быть числом

Можно ли выполнить модульное тестирование сервисов конфигурации, хотя они зависят от абстрактной базы класс и внешний файл .env? Потому что я знаю, что могу создать mockConfigService, но я думаю, что базовый класс ломает это. Я не знаю, как исправить этот тестовый файл.

1 Ответ

2 голосов
/ 20 апреля 2020

Основная проблема сводится к следующему: вы используете библиотеку Joi для анализа переменных среды. Каждый раз, когда вы вызываете validateValue, вызываются функции Joi, которые ожидают установки фактических переменных среды (в данном случае, SERVER_PORT). Теперь, когда эти переменные среды должны быть установлены, это допустимое предположение для работающей службы. Но в ваших тестовых случаях у вас не установлены переменные окружения, поэтому проверка Joi не удалась.

Примитивным решением было бы установить process.env.SERVER_PORT на какое-то значение в вашем beforeEach и удалить его в afterEach , Тем не менее, это всего лишь обходной путь к реальной проблеме.

Фактическая проблема: Вы жестко закодировали вызовы библиотек в BaseConfigurationService, которые предполагают, что переменные среды установлены. Ранее мы уже выяснили, что это неверное предположение при выполнении тестов. Когда вы сталкиваетесь с такими проблемами при написании тестов, это часто указывает на проблему жесткой связи.

Как мы можем решить это?

  1. Мы можем четко разделить проблемы и абстрагировать фактическую валидацию от ее собственного класса обслуживания, который используется BaseConfigurationService. Давайте назовем этот класс обслуживания ValidationService.
  2. Затем мы можем внедрить этот класс обслуживания в BaseConfigurationService, используя внедрение зависимостей Nest.
  3. При запуске тестов мы можем высмеивать 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 иначе, чем я, или даже разделить больше проблем на новые классы обслуживания.

...