【问题标题】:Unit Testing Angular 12 HTTP Interceptor expectOne fails单元测试 Angular 12 HTTP 拦截器 expectOne 失败
【发布时间】:2022-01-19 17:32:36
【问题描述】:

我有一个使用 Firebase 进行身份验证的 Angular 项目。为了向我们的服务器进行身份验证,我们需要来自 Firebase 的 JWT。该项目正在使用 Angular Fire 来完成此任务。

我们有一个 HTTP 拦截器,它拦截对服务器的任何请求,并添加所需的 Authorization 标头。拦截器似乎工作得很好,但我们也想对其进行单元测试。

我们的 AuthService 负责将操作委托给 Firebase。 getCurrentUserToken 方法将 JWT 作为 Promise 检索,因为 Angular Fire 会在需要时刷新 JWT。

这会使拦截器稍微复杂化,因为我们需要异步获取令牌并将其包装到来自请求的 Observable 中。

token-interceptor.ts

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    if (req.url.includes(environment.apiRoot)) {
      return defer(async () => {
        const token = await this.authService.getCurrentUserToken();
        const request = req.clone({
          headers: this.getDefaultHeaders(token),
          withCredentials: true,
        });
        return next.handle(request);
      }).pipe(mergeAll());
    } else {
      return next.handle(req);
    }
  }

  private getDefaultHeaders(accessToken: string): HttpHeaders {
    return new HttpHeaders()
      .set(
        'Access-Control-Allow-Origin',
        `${window.location.protocol}//${window.location.host}`
      )
      .set(
        'Access-Control-Allow-Headers',
        'Access-Control-Allow-Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Headers,Authorization,Accept,Content-Type,Origin,Host,Referer,X-Requested-With,X-CSRF-Token'
      )
      .set('Content-Type', 'application/json')
      .set('Accept', 'application/json')
      .set('Authorization', `Bearer ${accessToken}`);
  }
}

我们在 AppModule 中通过导入 HttpClientModule 并在 HTTP_INTERCEPTORS 注入令牌下提供拦截器来提供拦截器

  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true,
    },

问题只出现在我们的单元测试中

token-interceptor.spec.ts

describe(`TokenInterceptor`, () => {
  let service: SomeService;
  let httpMock: HttpTestingController;
  let authServiceStub: AuthService;

  beforeEach(() => {
    authServiceStub = {
      getCurrentUserToken: () => Promise.resolve('some-token'),
    } as AuthService;

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: TokenInterceptor,
          multi: true,
        },
        { provide: AuthService, useValue: authServiceStub },
      ],
    });

    service = TestBed.inject(SomeService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should add an Authorization header', () => {
    service.sendSomeRequestToServer().subscribe((response) => {
      expect(response).toBeTruthy();
    });

    const httpRequest = httpMock.expectOne({});

    expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
    expect(httpRequest.request.headers.get('Authorization')).toBe(
      'Bearer some-token'
    );

    httpRequest.flush({ message: "SUCCESS" });

    httpMock.verify();
  });
});

测试在httpMock.expectOne({}) 语句中失败并显示消息

Error: Expected one matching request for criteria "Match method: (any), URL: (any)", found none.

似乎我们的模拟请求根本没有发送。

但是,如果我们从 Testbed 中的提供程序中删除拦截器,则测试会在期望语句处失败,这表明正在找到模拟请求。

        Error: Expected false to equal true.
            at <Jasmine>
            at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:57:62)
            at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)
        Error: Expected null to be 'Bearer some-token'.
            at <Jasmine>
            at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:58:62)
            at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)

为什么expectOne 找不到我们的服务正在发送的请求?是拦截器本身的问题还是测试的构建方式?

【问题讨论】:

    标签: angular typescript unit-testing karma-jasmine angular-http-interceptors


    【解决方案1】:

    我认为问题出在拦截器上,因为我们可能没有等待const token = await this.authService.getCurrentUserToken();async 任务完成,然后再继续断言。

    在拦截器中添加这样的console.log:

    return defer(async () => {
            console.log('[Interceptor] Before token');
            const token = await this.authService.getCurrentUserToken();
            console.log('[Interceptor] After token');
            const request = req.clone({
              headers: this.getDefaultHeaders(token),
              withCredentials: true,
            });
            return next.handle(request);
          }).pipe(mergeAll());
    

    尝试使用 fakeAsync 并勾选:

    it('should add an Authorization header', fakeAsync(() => {
        service.sendSomeRequestToServer().subscribe((response) => {
          expect(response).toBeTruthy();
        });
    
        // Add a tick here to tell Angular to finish all promises encountered
        // in the code before carrying forward with the tests.
        tick();
        console.log('[Test] Before expectation');
        const httpRequest = httpMock.expectOne({});
    
        expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
        expect(httpRequest.request.headers.get('Authorization')).toBe(
          'Bearer some-token'
        );
    
        httpRequest.flush({ message: "SUCCESS" });
    
        httpMock.verify();
      }));
    

    如果没有tick,我怀疑你会看到[Interceptor] Before token,然后是[Test] Before expectation(错误的顺序)与勾选你应该看到[Interceptor] Before token[Interceptor] After token,然后是[Test] Before expectation(正确的顺序)

    【讨论】:

    • 成功了!你完全正确!现在,为什么 expectOne 无法找到该请求现在确实是有道理的。那么这是否会被认为是我的拦截器的糟糕设计,因为没有办法直接与它所代表的异步操作进行交互?或者上述情况合理吗?
    • 上述情况我想说是合理的。在处理 Promise 时,您最终将不得不使用 fakeAsynctick
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-28
    • 1970-01-01
    • 2020-05-10
    • 1970-01-01
    • 2023-03-13
    相关资源
    最近更新 更多