测试服务时未调用 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
);
}
}
此搜索控制器使用名为 HttpExceptionFilter 的 Filters。
只要抛出 HttpException,就会触发此过滤器。我创建了扩展 HttpException 的 ServiceException。每当出现错误时,我都会抛出新的 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);
})
})
你可以在上面
- 我正在实例化过滤器
- 我是直接调用catch方法
我有一个 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
);
}
}
此搜索控制器使用名为 HttpExceptionFilter 的 Filters。 只要抛出 HttpException,就会触发此过滤器。我创建了扩展 HttpException 的 ServiceException。每当出现错误时,我都会抛出新的 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);
})
})
你可以在上面
- 我正在实例化过滤器
- 我是直接调用catch方法