加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 服务器 > 安全 > 正文

angular-share运算符导致Jest测试失败

发布时间:2020-12-17 18:11:26 所属栏目:安全 来源:网络整理
导读:我有一个发出HTTP请求的Angular服务.该服务的主要工作是刷新访问令牌如果请求导致401,则重试该请求.该服务还能够宽限期地处理多个并发请求:如果有3个请求导致401,则该令牌将仅刷新一次,并且将重放所有3个请求. 以下GIF总结了此行为: 我的问题是我似乎无法
我有一个发出HTTP请求的Angular服务.该服务的主要工作是刷新访问令牌&如果请求导致401,则重试该请求.该服务还能够宽限期地处理多个并发请求:如果有3个请求导致401,则该令牌将仅刷新一次,并且将重放所有3个请求.
以下GIF总结了此行为:

enter image description here

我的问题是我似乎无法测试这种行为.最初,我的测试总是因超时而失败,因为我没有调用我的订阅或错误方法.添加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属性中的share运算符,它就可以工作.
??它可能需要做一些时间安排.与Jasmine不同,Jest并不会模仿RxJs使用的Date.now.
??一种可能的方法是尝试使用RxJs中的VirtualTimeScheduler来模拟时间,
??虽然这就是fakeAsync应该做的事情.

依赖关系和版本:

> Angular 6.1.0
> Rxjs 6.3.3
>开玩笑23.6.0
>节点10.0.0
> Npm 6.0.1

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

解决方法

我已经调查了这个,似乎我有一些想法,为什么它不适合你:

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

enter image description here

在您的测试连接中,当HttpClient异步代码中的连接取消订阅并关闭时,它仍将处于打开状态,以便下次创建新连接时.

要解决此问题,您还可以在异步代码中触发401错误:

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

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

service.post('controller',(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);
    }

因为我们不想在值上下文中抛出错误.

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

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

试试npm i和npm 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调试它

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读