如何对一个扩展了抽象类的类进行单元测试,读取环境变量。
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
的环境变量被设置的假设。前面我们已经搞清楚了,在运行测试时,这不是一个有效的假设。当你在编写测试时偶然发现这样的问题时,往往会指出紧耦合的问题。
我们如何解决这个问题呢?
- 我们可以将这些问题清晰地分开,并将实际的验证抽象掉,放到自己的服务类中,由
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. 在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
与我的做法不同,甚至将更多的关注点分离成新的服务类。
以上是关于如何对一个扩展了抽象类的类进行单元测试,读取环境变量。的主要内容,如果未能解决你的问题,请参考以下文章