如何对一个扩展了抽象类的类进行单元测试,读取环境变量。

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何对一个扩展了抽象类的类进行单元测试,读取环境变量。相关的知识,希望对你有一定的参考价值。

我想进行单元测试,并为我的 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.comquestions100959 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 "必须是一个数字。"SERVER_PORT "必须是一个数字。

有什么方法可以让我在配置服务依赖一个抽象基类和一个外部.env文件的情况下对它们进行单元测试?因为我知道我可以创建一个 mockConfigService 但我认为基类破坏了这一点。我不知道如何修复这个测试文件。

答案

主要的问题归结为以下几点。你使用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. 在BaseConfigurationService中注入ValidationServiceImpl。

现在,我们将删除验证逻辑,从 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. 实施一个模拟的验证服务。

为了测试的目的,我们不想对实际的环境变量进行验证,而只是单纯的接受所有的值。所以我们实现了一个模拟服务。

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.在测试中使用模拟服务。

最后,现在 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 将被使用。另外,除了修复你的测试,你还可以把关注点分离得干干净净。

我在这里提供的重构只是一个例子,你可以如何去做。我想,根据你进一步的用例,你可能会削减 ValidationService 与我的做法不同,甚至将更多的关注点分离成新的服务类。

以上是关于如何对一个扩展了抽象类的类进行单元测试,读取环境变量。的主要内容,如果未能解决你的问题,请参考以下文章

我应该对扩展 Sonata Base EntityManager 类的类进行单元测试吗?

如何对抽象类进行单元测试

扩展特征的单元测试类 - 我如何在特征中模拟和存根方法?

如何在 Swift 中对私有或内部函数进行单元测试?

如何对具有依赖项的工厂进行单元测试

在类扩展中声明的单元测试方法