共享运算符导致 Jest 测试失败
Posted
技术标签:
【中文标题】共享运算符导致 Jest 测试失败【英文标题】:share operator causes Jest test to fail 【发布时间】: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 应该做的。
依赖和版本:
-
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】:我对此进行了研究,似乎我有一些想法为什么它不适合你:
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 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
调试
【讨论】:
对于第 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/…以上是关于共享运算符导致 Jest 测试失败的主要内容,如果未能解决你的问题,请参考以下文章