【问题标题】:NestJs request and response interceptor unit testingNestJs 请求和响应拦截器单元测试
【发布时间】:2020-04-24 17:00:03
【问题描述】:

我想记录我的 API 的传入请求和传出响应。我创建了一个请求拦截器和一个响应拦截器,如此处所述

https://docs.nestjs.com/interceptors

所以请求拦截器只记录请求对象

@Injectable()
export class RequestInterceptor implements NestInterceptor {
  private readonly logger: Logger = new Logger(RequestInterceptor.name, true);

  public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const { originalUrl, method, params, query, body } = context.switchToHttp().getRequest();

    this.logger.debug({ originalUrl, method, params, query, body }, this.intercept.name);

    return next.handle();
  }
}

响应拦截器等待传出响应并稍后记录状态代码和响应对象

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  private readonly logger: Logger = new Logger(ResponseInterceptor.name, true);

  public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const { statusCode } = context.switchToHttp().getResponse();

    return next.handle().pipe(
      tap((responseData: any) =>
        this.logger.debug({ statusCode, responseData }, this.intercept.name),
      ),
    );
  }
}

我想测试它们,但不幸的是几乎没有测试经验。我尝试从请求拦截器开始并想出了这个

const executionContext: any = {
  switchToHttp: jest.fn().mockReturnThis(),
  getRequest: jest.fn().mockReturnThis(),
};

const nextCallHander: CallHandler<any> = {
  handle: jest.fn(),
};

describe('RequestInterceptor', () => {
  let interceptor: RequestInterceptor;

  beforeEach(() => {
    interceptor = new RequestInterceptor();
  });

  describe('intercept', () => {
    it('should fetch the request object', (done: any) => {
      const requestInterception: Observable<any> = interceptor.intercept(executionContext, nextCallHander);

      requestInterception.subscribe({
        next: value => {
          // ... ??? ...
        },
        error: error => {
          throw error;
        },
        complete: () => {
          done();
        },
      });
    });
  });
});

我目前不知道将什么传递给下一个回调,但是当我尝试按原样运行测试时,它说 requestInterception 变量未定义。所以在到达下一个回调之前测试失败了。所以我得到的错误信息是

TypeError: 无法读取未定义的属性“订阅”

我也尝试测试响应拦截器并想出了这个

const executionContext: any = {
  switchToHttp: jest.fn().mockReturnThis(),
  getResponse: jest.fn().mockReturnThis()
};

const nextCallHander: CallHandler<any> = {
  handle: jest.fn()
};

describe("ResponseInterceptor", () => {
  let interceptor: ResponseInterceptor;

  beforeEach(() => {
    interceptor = new ResponseInterceptor();
  });

  describe("intercept", () => {
    it("should fetch the statuscode and response data", (done: any) => {
      const responseInterception: Observable<any> = interceptor.intercept(
        executionContext,
        nextCallHander
      );

      responseInterception.subscribe({
        next: value => {
          // ...
        },
        error: error => {
          throw error;
        },
        complete: () => {
          done();
        }
      });
    });
  });
});

这次拦截器出错了

TypeError: 无法读取未定义的属性“管道”

有人介意帮我正确测试这两个拦截器吗?

提前致谢

【问题讨论】:

    标签: jestjs nestjs


    【解决方案1】:

    测试拦截器可能是测试 NestJS 应用程序中最具挑战性的部分之一,因为 ExecutionContext 并从 next 返回正确的值。

    让我们从ExecutionContext开始:

    你已经用你当前的上下文设置好了,重要的是你有一个 switchToHttp() 方法,如果你正在使用 HTTP(就像你一样)并且 switchToHttp() 返回的任何东西都有一个getResponse()getRequest() 方法(或两者都使用,如果两者都使用)。从那里,getRequest()getResponse() 方法应返回从 req 和 res 使用的值,例如 res.statusCodereq.originalUrl。我喜欢在同一个拦截器上传入和传出,所以我的 context 对象通常看起来像这样:

    const context = {
      switchToHttp: jest.fn(() => ({
        getRequest: () => ({
          originalUrl: '/',
          method: 'GET',
          params: undefined,
          query: undefined,
          body: undefined,
        }),
        getResponse: () => ({
          statusCode: 200,
        }),
      })),
      // method I needed recently so I figured I'd add it in
      getType: jest.fn(() => 'http')
    }
    

    这只是保持上下文轻松且易于使用。当然,您可以随时根据需要将这些值替换为更复杂的值以进行日志记录。

    现在是有趣的部分,CallHandler 对象。 CallHandler 有一个 handle() 函数,它返回一个 observable。至少,这意味着您的 next 对象需要看起来像这样:

    const next = {
      handle: () => of()
    }
    

    但这很基本,对记录响应或使用响应映射没有多大帮助。为了使处理函数更健壮,我们总是可以做类似的事情

    const next = {
      handle: jest.fn(() => of(myDataObject)),
    }
    

    现在,如果需要,您可以通过 Jest 覆盖该函数,但通常这就足够了。现在你的 next.handle() 将返回一个 Observable 并且可以通过 RxJS 操作符进行 pipable。

    现在要测试 Observable,您正在使用的订阅几乎是正确的,这太棒了!其中一项测试可能如下所示:

    describe('ResponseInterceptor', () => {
      let interceptor: ResponseInterceptor;
      let loggerSpy = jest.spyOn(Logger.prototype, 'debug');
    
      beforeEach(() => {
        interceptor = new ResponseInterceptor();
      });
    
      afterEach(() => {
        loggerSpy.resetMock();
      });
    
      describe('intercept', () => {
        it('should fetch the request object', (done: any) => {
          const responseInterceptor: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
    
          responseInterceptor.subscribe({
            next: value => {
              // expect the logger to have two parameters, the data, and the intercept function name
              expect(loggerSpy).toBeCalledWith({statusCode: 200, responseData: value}, 'intercept');
            },
            error: error => {
              throw error;
            },
            complete: () => {
              // only logging one request
              expect(loggerSpy).toBeCalledTimes(1);
              done();
            },
          });
        });
      });
    });
    

    其中executionContextcallHandler 来自我们上面设置的值。

    类似的想法可以用RequestInterceptor 完成,但只登录观察者的complete 部分(订阅回调),因为没有固有的数据点返回(尽管它仍然可以工作,由于observables 是如何工作的)。

    如果您想查看真实世界的示例(尽管是带有模拟创建库的示例),您可以 check out my code 获取我正在开发的日志包。

    【讨论】:

      猜你喜欢
      • 2022-10-14
      • 2021-11-22
      • 2020-01-03
      • 2020-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-10
      • 1970-01-01
      相关资源
      最近更新 更多