如何对扩展抽象类的类进行单元测试读取环境变量 [英] how to unit test a class extending an abstract class reading environment variables

查看:82
本文介绍了如何对扩展抽象类的类进行单元测试读取环境变量的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

但是正如您在第二个代码片段中看到的那样,我正在从构造函数中的基础服务调用函数.测试立即失败

ValidationError:"SERVER_PORT"必须为数字

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

解决方案

主要问题归结为:您正在使用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,甚至将更多的关注点分成新的服务类.

I want to get into unit testing and have some configuration services for my Nest API that I want to test. When starting the application I validate the environment variables with the joi package.

I have multiple configuration services for the database, the server, ... so I created a base service first. This one is able to read environment variables, parse the raw string to a desired datatype and validate the value.

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

Now I can extend this service with multiple configuration services. For the sake of simplicity I will take the server configuration service for this. Currently it only holds the port the application will listen to.

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

I found multiple articles out there that I should only test public methods, e.g.

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

so I'm assuming I should not test the methods from the base configuration service. But I would like to test the classes extending the base service. I started with this

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

but as you can see in the second code snippet I'm calling the functions from the base service in the constructor. The test instantly fails with

ValidationError: "SERVER_PORT" must be a number

Is there a way I can unit test the configuration services although they depend on an abstract base class and an external .env file? Because I know I can create a mockConfigService but I think the base class breaks this. I don't know how to fix this test file.

解决方案

The main problem boils down to this: You are using the Joi libary to parse environment variables. Whenever you call validateValue, Joi functions are called that expect actual environment variables to be set (in this case, SERVER_PORT). Now that these environment variables need to be set is a valid assumption for the running service. But in your test cases, you have no environment variables set, hence the Joi validation fails.

A primitive solution would be to set process.env.SERVER_PORT to some value in your beforeEach and delete it in afterEach. However, this is just a work-around around the actual issue.

The actual issue is: You hard-coded library calls into your BaseConfigurationService that have the assumption that environment variables are set. We already figured out earlier that this is not a valid assumption when running tests. When you stumble upon issues like this when writing tests, it often points to a problem of tight coupeling.

How can we address that?

  1. We can separate the concerns clearly and abstract away the actual validation into its own service class that's used by BaseConfigurationService. Let's call that service class ValidationService.
  2. We can then inject that service class into BaseConfigurationService using Nest's dependency injection.
  3. When running tests, we can mock the ValidationService so it does not rely on actual environment variables, but, for example, just doesn't complain about anything during validation.

So here's how we can achieve that, step by step:

1. Define a ValidationService interface

The interface simply describes how a class needs to look that can validate values:

import { AnySchema } from '@hapi/joi';

export interface ValidationService {
  validateValue<TValue>(value: TValue, validator: AnySchema, label: string): void;
}

2. Implement the ValidationService

Now we'll take the validation code from your BaseConfigurationService and use it to implemente 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. Inject ValidationServiceImpl into BaseConfigurationService

We'll now remove the validation logic from the BaseConfigurationService and instead add a call to 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. Implemente a mock ValidationService

For testing purposes, we don't want to validate against actual environment variables, but just genereally accept all values. So we implement a mock service:

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. Adapt classes extending BaseConfigurationService to have ConfigurationServiceImpl injected and pass it on to 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. use the mock service in the test

Finally, now that ValidationServiceImpl is a dependency of BaseConfigurationService, we use the mocked version in the test:

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

Now when running the tests, ValidationMockService will be used. Plus, apart from fixing your test, you also have a clean separation of concerns.

The refactoring I provided here is just an example how you can go ahead. I guess that, depending on your further use cases, you might cut ValidationService differently than I did, or even separate more concerns into new service classes.

这篇关于如何对扩展抽象类的类进行单元测试读取环境变量的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆