【问题标题】:share operator causes Jest test to fail共享运算符导致 Jest 测试失败
【发布时间】:2019-03-17 11:27:49
【问题描述】:

我有一个发出 HTTP 请求的 Angular 服务。该服务的主要工作是刷新访问令牌并在请求导致 401 时重试请求。该服务还能够优雅地处理多个并发请求:如果有 3 个请求导致 401,则令牌只会刷新一次,所有 3 个请求都将被重放。 以下 GIF 总结了这种行为:

我的问题是我似乎无法测试这种行为。最初,我的测试总是因超时而失败,因为我的订阅或错误方法没有被调用。添加 fakeAsync 后,我不再超时,但仍然没有调用我的观察者。我还注意到,如果我从 tokenObservable 中删除共享运算符,则会调用我的测试订阅,但这样做我将失去多播的好处。

这是无法正常工作的测试

it('refreshes token when getting a 401 but gives up after 3 tries', fakeAsync(() => {
const errorObs = new Observable(obs => {
  obs.error({ status: 401 });
}).pipe(
  tap(data => {
    console.log('token refreshed');
  })
);
const HttpClientMock = jest.fn<HttpClient>(() => ({
  post: jest.fn().mockImplementation(() => {
    return errorObs;
  })
}));
const httpClient = new HttpClientMock();

const tokenObs = new Observable(obs => {
  obs.next({ someProperty: 'someValue' });
  obs.complete();
});

const AuthenticationServiceMock = jest.fn<AuthenticationService>(() => ({
  refresh: jest.fn().mockImplementation(() => {
    return tokenObs;
  })
}));
const authenticationService = new AuthenticationServiceMock();

const service = createSut(authenticationService, httpClient);

service.post('controller', {}).subscribe(
  data => {
    expect(true).toBeFalsy();
  },
  (error: any) => {
    expect(error).toBe('random string that is expected to fail the test, but it does not');
    expect(authenticationService.refresh).toHaveBeenCalledTimes(3);
  }
);
}));

这就是我在我的 SUT 中注入模拟的方式:

  const createSut = (
    authenticationServiceMock: AuthenticationService,
    httpClientMock: HttpClient
  ): RefreshableHttpService => {
    const config = {
      endpoint: 'http://localhost:64104',
      login: 'token'
    };
    const authConfig = new AuthConfig();

    TestBed.configureTestingModule({
      providers: [
        {
          provide: HTTP_CONFIG,
          useValue: config
        },
        {
          provide: AUTH_CONFIG,
          useValue: authConfig
        },
        {
          provide: STATIC_HEADERS,
          useValue: new DefaultStaticHeaderService()
        },
        {
          provide: AuthenticationService,
          useValue: authenticationServiceMock
        },
        {
          provide: HttpClient,
          useValue: httpClientMock
        },
        RefreshableHttpService
      ]
    });

    try {
      const testbed = getTestBed();
      return testbed.get(RefreshableHttpService);
    } catch (e) {
      console.error(e);
    }
  };

下面是被测系统的相关代码:

    @Injectable()
export class RefreshableHttpService extends HttpService {
  private tokenObservable = defer(() => this.authenthicationService.refresh()).pipe(share());
  constructor(
    http: HttpClient,
    private authenthicationService: AuthenticationService,
    injector: Injector
  ) {
    super(http, injector);
  }
  public post<T extends Response | boolean | string | Array<T> | Object>(
    url: string,
    body: any,
    options?: {
      type?: { new (): Response };
      overrideEndpoint?: string;
      headers?: { [header: string]: string | string[] };
      params?: HttpParams | { [param: string]: string | string[] };
    }
  ): Observable<T> {
    return defer<T>(() => {
      return super.post<T>(url, body, options);
    }).pipe(
      retryWhen((error: Observable<any>) => {
        return this.refresh(error);
      })
    );
  }

  private refresh(obs: Observable<ErrorResponse>): Observable<any> {
    return obs.pipe(
      mergeMap((x: ErrorResponse) => {
        if (x.status === 401) {
          return of(x);
        }
        return throwError(x);
      }),
      mergeScan((acc, value) => {
        const cur = acc + 1;
        if (cur === 4) {
          return throwError(value);
        }
        return of(cur);
      }, 0),
      mergeMap(c => {
        if (c === 4) {
          return throwError('Retried too many times');
        }

        return this.tokenObservable;
      })
    );
  }
}

以及它继承自的类:

 @Injectable()
export class HttpService {
  protected httpConfig: HttpConfig;
  private staticHeaderService: StaticHeaderService;
  constructor(protected http: HttpClient, private injector: Injector) {
    this.httpConfig = this.injector.get(HTTP_CONFIG);
    this.staticHeaderService = this.injector.get(STATIC_HEADERS);
  }

由于某种未知原因,它在第二次调用时无法解析由 refresh 方法返回的 observable。 奇怪的是,如果您从 SUT 的 tokenObservable 属性中删除共享运算符,它就会起作用。 它可能与时间有关。与 Jasmine 不同,Jest 不会模拟 RxJs 使用的 Date.now。 一种可能的方法是尝试使用 RxJs 中的 VirtualTimeScheduler 来模拟时间, 虽然这是 fakeAsync 应该做的。

依赖和版本:

  1. Angular 6.1.0
  2. Rxjs 6.3.3
  3. 开玩笑 23.6.0
  4. 节点 10.0.0
  5. Npm 6.0.1

以下文章帮助我实现了该功能: RxJS: Understanding the publish and share Operators

【问题讨论】:

    标签: angular rxjs jestjs


    【解决方案1】:

    我对此进行了研究,似乎我有一些想法为什么它不适合你:

    1) Angular HttpClient 服务在异步代码中引发错误,但您是同步执行的。结果,它破坏了共享运算符。如果您可以调试,您可以通过查看ConnectableObservable.ts来查看问题

    在您的测试连接将仍然打开,而 HttpClient 异步代码中的连接取消订阅并关闭,以便下次创建新连接。

    要修复它,您还可以在异步代码中触发 401 错误:

    const errorObs = new Observable(obs => {
       setTimeout(() => {
         obs.error({ status: 404 });
       });
    ...
    

    但您必须等待所有异步代码已使用tick 执行:

    service.post('controller', {}).subscribe(
      data => {
        expect(true).toBeFalsy();
      },
      (error: any) => {
        expect(error).toBe('Retried too many times');
        expect(authenticationService.refresh).toHaveBeenCalledTimes(3);
      }
    );
    
    tick(); // <=== add this
    

    2) 你应该删除RefreshableHttpService 中的以下表达式:

    mergeScan((acc, value) => {
        const cur = acc + 1;
        if (cur === 4) { <== this one
          return throwError(value);
        }
    

    因为我们不想在value 上下文中抛出错误。

    之后,您应该捕获所有刷新调用。

    我还创建了示例项目https://github.com/alexzuza/angular-cli-jest

    试试npm inpm t

      Share operator causes Jest test to fail
        √ refreshes token when getting a 401 but gives up after 3 tries (41ms)
    
      console.log src/app/sub/service.spec.ts:34
        refreshing...
    
      console.log src/app/sub/service.spec.ts:34
        refreshing...
    
      console.log src/app/sub/service.spec.ts:34
        refreshing...
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        4.531s, estimated 5s
    Ran all test suites.
    

    也可以通过npm run debug调试

    【讨论】:

    • 对于第 1 条观察:我更新了问题以包含创建被测系统的代码。它表明我在嘲笑 Angular 的 HttpClient。关于观察 2:如果我得到 401 超过 3 次,我想扔 401。无休止地重试会失败的东西是没有意义的。
    • no 1. 你在模拟 HttpClient,但它不能作为基础 HttpClient。否 2 您在下一个 mergeMap 运算符中抛出错误。应该够了。
    • 你试过我的建议了吗?这个对我有用。它尝试刷新令牌 3 次,然后抛出错误 Retried too many times
    • RefreshableHttpService 继承自 HttpService,我在其中注入了 Angular 的 HttpClient,但在我的测试中,我注入了一个 mock。我将更新问题以包含它
    • 我没有在这里使用TestBed,所以它就像隔离测试vsavkin.com/…
    猜你喜欢
    • 2018-06-03
    • 1970-01-01
    • 2020-05-11
    • 1970-01-01
    • 2019-10-23
    • 2020-05-05
    • 2019-03-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多