在我的 Angular 服务中,我有一些代码执行轮询以显示微调器,直到满足特定条件:
@Inject({…})
export class SomeService {
backendCall(): Observable<SomeStatusWrapper> {
return this.http.get(…)
}
isDocumentReady(): Observable<boolean> {
const hasFinished = new Subject<boolean>()
return interval(1000).pipe(
switchMap(idx => of(idx).pipe(delay(idx*250))), // Increment delay between retries
switchMap(() => this.backendCall()),
map(({status}) => status === 'Done'),
tap(done => done?hasFinished.next(done) : undefined),
distinctUntilChanged(),
takeUntil(hasFinished),
endWith(true)
)
}
}
现在,我想为此添加一个单元测试,我正在使用 Jasmine 弹珠,但我不确定那里会有多少帧,我无法让单元测试正常工作。
如何用大理石语法表达“间隔”?我知道我们可以做像
cold('1000ms (a|)',{a:true})
这样的事情,它等待 1000 毫秒,发出“真”并同时完成。
我假设这个测试会通过但它失败了:
it('should emit false after 1000ms', () => {
const service = TestBed.inject(service) // already mocked backendCall to return a status != 'Done'
expect(service.isDocumentReady()).toBeObservable(cold('1000ms f', {f: false}))
})
如何编写茉莉花弹珠才能通过此测试?
describe('', () => {
it('should pass', () => {
const stream$ = interval(1000).pipe(
switchMap(val => of(val).pipe(delay(250*val))),
mapTo(false)
)
expect(stream$).toBeObservable(cold(???, {f:false}))
})
})
我觉得问题是对
toBeObservable
的调用是同时订阅和断言的,但是在断言之前需要提前时间。这个假设是基于调试 jasmine 和 jasmine-marbles 来尝试理解幕后真正发生的事情。
使用测试调度程序,可以重新组织代码,以便订阅开始并在之后进行断言:
expectObservable(partner$).toBe('500ms (a|)', {a})
我有一个在 Angular 应用程序中类似工作的例子。有一个服务具有以下返回可观察值的方法:
getPartner() {
return of(1).pipe(
delay(500),
map(() => this.newPartner())
)
}
getPartnerUsingInterval() {
return interval(500).pipe(
take(1),
map(() => this.newPartner())
)
}
以下测试通过:
import { discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { delay, interval, of, take } from 'rxjs';
import { InsuranceService } from './insurance.service';
import { Partner } from './model';
...
// idea from https://kevinkreuzer.medium.com/marble-testing-with-rxjs-testing-utils-3ae36ac3346a
it('getPartner should return after some time', fakeAsync(() => {
// this example works when using interval, delay, etc.
getTestScheduler().run(({cold, expectObservable}) => {
tick(500) // call anytime before call to `expectObservable`
const partner$ = service.getPartner();
expectObservable(partner$).toBe('500ms (a|)', {a})
})
}))
将它与以下测试进行比较,这些测试的结构类似于 OP 中的测试,但这些测试不起作用。评论试图解释原因。
// the following doesnt work, because it subscribes after the results are collected
xit('FAILS getPartner should return after some time', fakeAsync(() => {
const partner$ = service.getPartner();
const expected = cold('500ms (a|)', { a });
tick(500); // TOO EARLY
// doesn't work either:
getTestScheduler().expectObservable(partner$) // doesnt seem to subscribe yet, probably because not inside `run`
.toBe('500ms (a|)', {a})
// with result:
// Failed:
// Expected
//
// to deep equal
// {"frame":500,"notification":{"kind":"N","value":{"id":"1","firstName":"John","lastName":"Smith","address":{"nr":1,"street":"Sesame Street","zip":"1000","city":"Lausanne","state":"Vaud"}}}}
// {"frame":500,"notification":{"kind":"C"}}
//
// which is saying that the actual value is totally empty and so not equal to the expectation
}));
// the following doesnt work, because it subscribes after the results are collected
xit('FAILS getPartner should return after some time', fakeAsync(() => {
const partner$ = service.getPartner();
tick(500); // TOO EARLY
getTestScheduler().flush()
flush();
const expected = cold('500ms (a|)', { a });
tick(500); // TOO EARLY
expect(partner$) // presumably does not yet subscribe
.toBeObservable(expected); // presumably subscribes and tests in one step, and tick() needs to be called after subscribing, as shown below when manually subscribing
tick(500); // TOO LATE - it happens after the actual subscription. while debugging, we see that the subscription is used, but it's simply too late
// fails with:
// Expected $.length = 0 to equal 2.
// Expected $[0] = undefined to equal Object({ frame: 500, notification: Object({ kind: 'N', value: Object({ id: '1', firstName: 'John', lastName: 'Smith', address: Object({ nr: 1, street: 'Sesame Street', zip: '1000', city: 'Lausanne', state: 'Vaud' }) }), error: undefined }) }).
// Expected $[1] = undefined to equal Object({ frame: 500, notification: Object({ kind: 'C', value: undefined, error: undefined }) }).
// meaning we expected it to contain a partner and a completion, but neither was present
// useful if it fails because of: `Error: 1 periodic timer(s) still in the queue.`, but in this case it is not necessary if we add enough tick() calls
// discardPeriodicTasks()
}));
以下测试手动订阅,有助于解释上述测试失败的原因:
it('getPartner should return after some time using manual subscription', fakeAsync(() => {
let partner: Partner | undefined
const partner$ = service.getPartner();
// this manual subscription works and shows that the tick() needs to be called after subscription
partner$.subscribe(p => partner = p)
expect(partner).toBe(undefined);
tick(500);
expect(partner).toEqual(a);
}));