如何使用 Jasmine Marbles 测试永无止境的流?

问题描述 投票:0回答:1

在我的 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}))
  })
})
angular unit-testing rxjs jasmine-marbles
1个回答
0
投票

我觉得问题是对

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);
}));
© www.soinside.com 2019 - 2024. All rights reserved.