【问题标题】:Marble testing rxjs observable on an angular service大理石测试 rxjs 在角度服务上可观察
【发布时间】:2021-05-20 21:31:53
【问题描述】:

我有一个带有暴露 observable 的 Angular 服务,我正在尝试使用 rxjs TestScheduler 进行大理石测试。服务上的一个方法控制 observable 发出的值,使用 BehaviorSubject 作为源。这是一个非常简单的例子:

import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { BehaviorSubject } from "rxjs";

@Injectable()
export class MyService {
  private _isVisibleSubject: BehaviorSubject<boolean> = new BehaviorSubject<
    boolean
  >(true);

  isVisible$: Observable<boolean> = this._isVisibleSubject.asObservable();

  constructor() {}

  toggleVisibility() {
    this._isVisibleSubject.next(!this._isVisibleSubject.value);
  }
}

MyService 的简单单元测试。我想测试 2 个条件:

  1. isVisible$ observable 以 true 开头
  2. toggleVisibility 被调用两次时,isVisible$ 有 3 个值,true、false 和 back to true

这是测试类

import { TestBed } from '@angular/core/testing';
import { TestScheduler } from 'rxjs/testing';

import { MyService } from './my.service';

describe('MyService ', () => {

  let service: MyService ;
  const testScheduler: TestScheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });

  beforeEach(() => {
    TestBed.configureTestingModule({ providers: [MyService] });
    service = TestBed.inject(MyService);
  });

  it('Should start out visible', () => {

    // This one is easy enough, it starts out visible and nothing else happens

    testScheduler.run((helpers) => {

      const { expectObservable } = helpers;

      const values: {[key: string]: boolean} = {'a': true};
      const expected = 'a';

      expectObservable(service.isVisible$).toBe(expected, values);
    });


  it('Should toggle visibility back and forth', () => {

    testScheduler.run((helpers) => {

      const { expectObservable } = helpers;

      const values: {[key: string]: boolean} = {'a': true, 'b': false};
      const expected = 'aba';  // I've also tried with various frames between, eg '-a-b-a'

      service.toggleVisibility();
      service.toggleVisibility();

      expectObservable(service.isVisible$).toBe(expected, values);
    });
  });

当我运行它时,第一个测试通过,但第二个测试失败并出现类似Expected $.length = 1 to equal 3. 的错误我已经通过运行如下测试验证了该值实际上发生了变化:

    testScheduler.run((helpers) => {

      const { expectObservable } = helpers;

      const values: {[key: string]: boolean} = {'b': false};
      const expected = 'b';

      service.toggleVisibility();

      expectObservable(service.isVisible$).toBe(expected, values);
    });

我假设问题是 service.isVisible$ 在调用 expectObservable() 之前没有被订阅,所以以前的值会丢失。我查看了rxjs documentation on marble testing,但无法弄清楚。

我意识到我可以在测试开始时手动设置对 isVisible$ 可观察对象的订阅,并在我进行更改时验证状态,但这并不像使用大理石测试那样好。

我想做的事可能吗?

【问题讨论】:

  • 您的示例代码在拼写上有一些不一致,但我认为这不是问题所在。这很可能是滴答声的问题——您需要等待一个滴答声让 JS 引擎能够对更改消息采取行动,因为 JS 并不是真正的多线程。我不会使用主题的值来切换——我会使用具有即时状态更改的单独字段,切换该字段,然后在 .next 调用中将新的切换值作为消息发送。
  • 另外,请注意 Angular 有一些预构建的测试类来处理需要滴答的事情:tick,它也是 @angular/core 测试库的一部分。
  • @ps2goat 感谢您对不一致之处的提醒。我急忙直接在SO中写作。我希望避免使用 tick() 和 fakeAsync 东西,因为它总是让我觉得有点脏.. 例如:延迟一些任意时间以希望任务完成,但如果使用大理石测试是不可能的,我'必须走那条路
  • @ps2goat 我认为滴答声和相关内容没有问题,因为不涉及异步操作。

标签: angular unit-testing rxjs rxjs-observables


【解决方案1】:

这是一个非常有趣的问题,我将分享我对它为什么不起作用的看法。

所以,当到达这些行时:

service.toggleVisibility();
service.toggleVisibility();

service.isVisible$ observable 尚未被订阅。更具体地说,它派生自的 BehaviorSubject 没有订阅者。

那么,当行

expectObservable(service.isVisible$).toBe(expected, values);

到达时,BehaviorSubject 终于有了它的第一个订阅者,并将发出正确的值。测试未通过,因为您预计会发出 3 个值,但实际上只有一个。

解决这个问题的方法可能是:

 it('Should toggle visibility back and forth', () => {
  testScheduler.run((helpers) => {

    const { expectObservable } = helpers;

    const values: {[key: string]: boolean} = {'a': true, 'b': false};
    const expected = 'aba';

    const src$ = merge(
      // the other `source` which defines the changes in time
      of(...expected).pipe(
        // only 2 toggle operations are needed
        // because `service.isVisible$` starts with `true`
        take(2),
        
        tap(v => service.toggleVisibility())

        // with this, `.next()` notifications will be ignored
        // which is what we want, because we only subscribed to this in order to
        // simulate changes that occur in time
        ignoreElements(),
      )
      
      // the observable whose values we want to examine
      service.isVisible$,
    )

    expectObservable(src$).toBe(expected, values);
  });
})

请注意,如果您想要更复杂的时间范围,例如 a---b---a---10s--c,您可以将 of(...) 替换为 cold(timePattern, values)

【讨论】:

  • 感谢您确认我对它为什么不起作用的怀疑。我试了一下你的解决方案,但我得到了相同的结果,它只返回 1 个值。
  • @aasukisuki 抱歉,我认为您可以这样做:在“合并”参数中将“service.isVisible”放在“of(...)...”之前。
猜你喜欢
  • 1970-01-01
  • 2021-04-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-23
  • 2023-03-16
  • 1970-01-01
相关资源
最近更新 更多