测试服务时未调用 NestJS UseFilter

NestJS UseFilter not getting called when testing Service

我有一个 NestJS 控制器: search.controller.ts

import { Body, Controller, Post, Req, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from '../exception/http-exception.filter';
import { SearchData } from './models/search-data.model';
import { SearchResults } from 'interfaces';
import { SearchService } from './search.service';

@Controller('')
@UseFilters(HttpExceptionFilter)
export class SearchController {
  constructor(private searchService: SearchService) {}

  @Post('api/search')
  async searchDataById(
    @Body() searchData: SearchData,
    @Req() req
  ): Promise<SearchResults> {
    return await this.searchService.getSearchResultsById(
      searchData,
      token
    );
  }
}

此搜索控制器使用名为 HttpExceptionFilterFilters。 只要抛出 HttpException,就会触发此过滤器。我创建了扩展 HttpExceptionServiceException。每当出现错误时,我都会抛出新的 ServiceException()

HttpExceptionFilter

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException
} from '@nestjs/common';
import { ErrorDetails } from './error-details.interface';
import { HTTP_ERRORS } from './errors.constant';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();
    const api = exception.getResponse() as string;
    const errorDetails = this.getErrorDetails(api, status);

    response.status(status).json({
      status: status,
      title: errorDetails.title,
      message: errorDetails.message
    });
  }

  private getErrorDetails(api: string, status: string | number): ErrorDetails {
    const errorDetails: ErrorDetails = {
      title: HTTP_ERRORS.GENERAL.ERROR.title,
      message: HTTP_ERRORS.GENERAL.ERROR.message
    };

    // if rejection status is logged out or toke expired then redirect to login

    if (
      HTTP_ERRORS.hasOwnProperty(api) &&
      HTTP_ERRORS[api].hasOwnProperty(status)
    ) {
      errorDetails.title = HTTP_ERRORS[api][status].title;
      errorDetails.message = HTTP_ERRORS[api][status].message;
    }

    return errorDetails;
  }
}

ServiceException

import { HttpException } from '@nestjs/common';

export class ServiceException extends HttpException {
  constructor(private details, private code) {
    super(details, code);
  }
}

search.service.ts

import { APIS } from '../app.constants';
import { HttpService, HttpStatus, Injectable } from '@nestjs/common';
import { SearchData, SearchResultSchema } from './models/search-data.model';
import { AppConfigService } from '../app-config/app-config.service';
import { AxiosResponse } from 'axios';
import { DataMappingPayload } from './models/data-mapping-payload.model';
import { SEARCH_SCHEMAS } from './search.constants';
import { SearchModelMapper } from './search-model-mapper.service';
import { SearchResults } from '@delfi-data-management/interfaces';
import { ServiceException } from '../exception/service.exception';

@Injectable()
export class SearchService {
  constructor(
    private searchModelMapper: SearchModelMapper,
    private configService: AppConfigService,
    private readonly httpService: HttpService
  ) {}

  // eslint-disable-next-line max-lines-per-function
  async getSearchResultsById(
    searchData: SearchData,
    stoken: string
  ): Promise<SearchResults> {
    if (searchData.filters.collectionId && searchData.viewType) {
      if (
        Object.values(SEARCH_SCHEMAS).indexOf(
          searchData.viewType as SEARCH_SCHEMAS
        ) !== -1
      ) {
        try {

         ...... some code cant paste here
          return this.searchModelMapper.generateSearchResults(
            kinds,
            mappingPayload,
            searchResultsAPI.data.results
          );
        } catch (error) {
          throw new ServiceException(
            APIS.SEARCH,
            HttpStatus.INTERNAL_SERVER_ERROR
          );
        }
      } else {
        throw new ServiceException(APIS.SEARCH, HttpStatus.BAD_REQUEST);
      }
    } else if (!searchData.filters.collectionId) {
      throw new ServiceException(APIS.SEARCH, HttpStatus.BAD_REQUEST);
    } else {
      throw new ServiceException(APIS.SEARCH, HttpStatus.BAD_REQUEST);
    }
  }

现在事情永远不会到达单元测试

中的HttpExceptionFilter文件

search.service.spec.ts

beforeEach(async () => {
    const app = await Test.createTestingModule({
      imports: [AppConfigModule, HttpModule, SearchModule]
    }).compile();
    searchService = app.get<SearchService>(SearchService);
  });

it('should throw error message if viewType not provided', () => {
      const searchDataquery = {
        filters: {
          collectionId: 'accd'
        },
        viewType: ''
      };
      const result = searchService.getSearchResultsById(searchDataquery, 'abc');
      result.catch((error) => {
        expect(error.response).toEqual(
          generateSearchResultsErrorResponse.viewTypeError
        );
      });
    });

有没有抛出内部触发HttpException的new ServiceException不触发HttpExceptionFilter的原因?

过滤器在单元测试期间未绑定,因为它们需要 Nest 正确绑定的请求上下文(这是 Nest 处理请求生命周期的方式)。由于单元测试没有传入的 HTTP 请求,因此生命周期仅被视为您显式调用的内容,在本例中为:SearchSerivce。如果您要测试过滤器,您应该设置 e2e 类型测试,在其中使用 supertest 发送 HTTP 请求并允许您的过滤器在请求期间进行捕获。

我在不同的环境中需要类似的东西。在我的例子中,我需要测试 graphql 的自定义过滤器。此过滤器正在捕获解析器内部抛出的 HttpException。

这是我的测试样本

import { HttpException, HttpStatus, Logger } from '@nestjs/common'
import {
  ApolloError,
  AuthenticationError,
  ForbiddenError,
} from 'apollo-server-errors';
import {ApolloExceptionFilter} from './apollo-exeption.filter'
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'

describe('ApolloExceptionFilter', () => {
  const filter = new ApolloExceptionFilter(new Logger());
  const host  = new ExecutionContextHost([], null, null);
  host.setType('graphql');

  it('should throw apollo AuthenticationError', () => {
    const t = () => {

     filter.catch(new HttpException({}, HttpStatus.UNAUTHORIZED), host);
    };
    expect(t).toThrow(AuthenticationError);

  })
})

你可以在上面

  1. 我正在实例化过滤器
  2. 我是直接调用catch方法