оператор share вызывает сбой Jest-теста - PullRequest
0 голосов
/ 12 октября 2018

У меня есть служба Angular, которая делает HTTP-запросы.Основная задача службы - обновить токен доступа и повторить запрос, если запрос приводит к 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);
  }

По какой-то неизвестной причине он не разрешает наблюдаемую информацию, возвращаемую методом обновления при втором вызове.Как ни странно, это работает, если вы удалите оператор share из свойства tokenObservable из SUT.Возможно, придется что-то делать со временем.В отличие от Жасмин, Jest не высмеивает Date.now, которым пользуются RxJ.Возможный путь - попытаться использовать VirtualTimeScheduler из RxJs для имитации времени, хотя это то, что должен делать fakeAsync.

Зависимости и версии:

  1. Angular 6.1.0
  2. Rxjs 6.3.3
  3. Jest 23.6.0
  4. Узел 10.0.0
  5. Npm 6.0.1

Следующая статья помогла мне реализовать функциональность: RxJS: Понимание операторов публикации и обмена

1 Ответ

0 голосов
/ 13 октября 2018

Я рассмотрел это и, похоже, у меня есть некоторые идеи, почему он не работает для вас:

1) Служба Angular HttpClient выдает ошибку в асинхронном коде, но вы сделали это синхронно.В результате нарушается доля оператора.Если вы можете отладить, вы можете увидеть проблему, посмотрев на ConnectableObservable.ts

enter image description here

В вашем тестовом соединении все еще будет открыто, пока соединение в HttpClient asyncкод отменяет подписку и закрывается, поэтому в следующий раз будет создано новое соединение.

Чтобы исправить это, вы также можете вызвать ошибку 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 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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...