【问题标题】:how to unit test a class extending an abstract class reading environment variables如何对扩展抽象类读取环境变量的类进行单元测试
【发布时间】:2020-04-20 20:20:12
【问题描述】:

我想进行单元测试,并为我想要测试的 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
        );
    }
}

我发现多篇文章我应该只测试公共方法,例如

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

但正如您在第二个代码 sn-p 中看到的那样,我正在从构造函数中的基本服务调用函数。测试立即失败

ValidationError: "SERVER_PORT" 必须是数字

有没有一种方法可以对配置服务进行单元测试,尽管它们依赖于抽象基类和外部 .env 文件?因为我知道我可以创建一个mockConfigService,但我认为基类打破了这一点。我不知道如何修复这个测试文件。

【问题讨论】:

  • 您是否尝试检查像expect(serverConfigurationService).not.toThrow(ValidationError); 这样的 trown 错误?

标签: node.js unit-testing jestjs nestjs


【解决方案1】:

主要问题归结为:您正在使用 Joi 库来解析环境变量。每当您调用 validateValue 时,都会调用 Joi 函数,这些函数期望设置实际的环境变量(在本例中为 SERVER_PORT)。现在需要设置这些环境变量是运行服务的有效假设。但是在您的测试用例中,您没有设置环境变量,因此 Joi 验证失败。

一个原始的解决方案是将process.env.SERVER_PORT 设置为beforeEach 中的某个值,然后在afterEach 中将其删除。但是,这只是解决实际问题的方法。

实际问题是:您将库调用硬编码到 BaseConfigurationService 中,假设环境变量已设置。我们之前已经发现,在运行测试时,这不是一个有效的假设。当您在编写测试时偶然发现此类问题时,通常会指出紧密耦合的问题。

我们如何解决这个问题?

  1. 我们可以清楚地分离关注点并将实际验证抽象为BaseConfigurationService 使用的自己的服务类。让我们将该服务类称为ValidationService
  2. 然后我们可以使用 Nest 的依赖注入将该服务类注入到BaseConfigurationService
  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

我们现在将从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.在测试中使用模拟服务

最后,既然ValidationServiceImplBaseConfigurationService 的依赖,我们在测试中使用模拟版本:

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。此外,除了修复测试之外,您还可以清晰地分离关注点。

我在此处提供的重构只是一个示例,您可以如何继续。我想,根据您进一步的用例,您可能会以与我不同的方式削减 ValidationService,甚至将更多关注点分离到新的服务类中。

【讨论】:

  • 您提供的答案对初学者来说真的很棒!谢谢。最后一个问题:您将模拟文件(如类、数据结构等)放在哪里?我认为它们不应该存在于src 目录中,因为它们会影响构建大小.. ?
  • 这取决于个人喜好,我猜。我喜欢将我的模拟类放在它们的接口和“适当的”实现旁边,以将属于一个域的东西放在一起。不过,我认识其他人,他们更喜欢将所有模拟类与测试数据一起放在 lib-test 文件夹中。我想您可能会争辩说,这种方法可以更清晰地区分测试代码和生产代码。
猜你喜欢
  • 2011-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-14
  • 2011-10-21
  • 2019-11-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多