У меня есть служба Angular, которая делает HTTP-запросы.Основная задача службы - обновить токен доступа и повторить запрос, если запрос приводит к 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);
}
По какой-то неизвестной причине он не разрешает наблюдаемую информацию, возвращаемую методом обновления при втором вызове.Как ни странно, это работает, если вы удалите оператор share из свойства tokenObservable из SUT.Возможно, придется что-то делать со временем.В отличие от Жасмин, Jest не высмеивает Date.now, которым пользуются RxJ.Возможный путь - попытаться использовать VirtualTimeScheduler из RxJs для имитации времени, хотя это то, что должен делать fakeAsync.
Зависимости и версии:
- Angular 6.1.0
- Rxjs 6.3.3
- Jest 23.6.0
- Узел 10.0.0
- Npm 6.0.1
Следующая статья помогла мне реализовать функциональность: RxJS: Понимание операторов публикации и обмена