RxJS 测试之弹珠图

1,463 阅读5分钟

简单observable测试案例

对于简单的Observable 对象,测试其内部逻辑正确性,可以直接订阅对比输出结果是否正确。

先定义一个变量用来接收订阅数据,然后订阅Observable取值之后,再断言判断是否正确

    let target;
    of(2).subscribe(x=>target=x)
    expect(target).toBe(2)

异步observable 测试案例

但是碰到异步的 Observable ,这种测试逻辑就会非常的复杂。

如下面这个,如何去测试每2毫秒秒中发出的值?

    const target = interval(2).pipe(take(4));

弹珠图就能够非常简单的以一种优雅的方式测试,下面我们将先介绍弹珠语法,然后再解析这个案例

     const intput = '--a-b-c-(d|)';
     const target = interval(2).pipe(take(4));
     expectObservable(target).toBe(intput, { a: 0, b: 1, c: 2, d: 3 });

弹珠语法

**' ' **空格:空格将会被忽略掉,主要是用来水平对齐

'-' 帧:一个符号代表一帧,用来模拟时间,一帧相当于 一毫秒

[0-9]+[ms|s|m] 时间语法:指定时间帧单位,可以用来代替帧,数字与单位之间不能有空格

| 终止:终止流符号,会发出 complete 信号,代表一个 observable 正常执行完成

# 错误:在observable 中发出 error 信号

[a-z0-9]: observable 中发出的值

():同步组,用来包裹observable 发出的值,表示是同步发出这些值

^ : 订阅

!: 取消订阅

异步测试案例解析

'-' 代表一帧,一毫秒 ,interval 配置2毫秒后发出值,所以开头需要 两个 '-' ,然后interval 发出 1 ,发出1 也需要1毫秒,然后再过一毫秒发出 2 ,同理 发出2 这个发出的过程也需要1 毫秒,因此在两个 '-' 后面接 a 用来模拟发出的第一个值1,然后只需要接一个 '-' 后面再接b 模拟发出的2。

take(4)的模拟,当发出第四个值的时候,observable 就会发出 complete 信号,代表终止。这里需要注意的是发出的第四个值3 与 complete 是同步发出的,因为模拟take(4)需要同步块将两者包裹起来。

const intput = '--a-b-c-(d|)';

下面我们以表格的形式解释这个流过程,其实这里你需要特别注意,发出值也会占用 1 毫秒的的时候,那么你就会很容易明白语法为什么要这么写。横坐标是发出的值,纵坐标是时间刻度。

Interval 值0123
弹珠模拟--a-b-c-d|
时间1ms2ms3ms4ms5ms6ms7ms8ms9ms

如果上面的你弄明白之后,那么你就会知道,如何模拟 interval(10) 呢?这个时候你肯定就会明白为什么a 与 b 之间是9 ms

const intput = '10ms a 9ms b 9ms c 9ms (d|)';
or
const intput = '- 9ms a 9ms b 9ms c 9ms (d|)';

案例2

 target = of(0, 1, 2, 3).pipe(map(a => a * 2));
 //correct
 input = '(abcd|)';
//error
 input = 'abcd|';

如何测试 map 中的逻辑,是否符合我们需求?

首先要清楚 of 在没有指定时间调度器的时候,发出值都是同步的,因此需要配置 () 同步组,不加括号就是异步模拟,是错误的。(注意,如果代码中有除了AsyncScheduler的 调度器,例如 promise 或者 使用 AsapScheduler/AnimationFrameScheduler/etc 调度器,测试就不能够很正确,因为这些不能够被TestScheduler 虚拟化 )

冷热 observable

Clod 与 Hot 是用来模拟两种observable 的,详细的可以看这篇博客 Hot and Clod

  1. 冷 observable 是只有当测试开始的时候这个 observable 订阅才刚开始发生 ,例如 of , from 等操作符。或者在 observable 内部 原生的使用 next 方法产生数据,是单播传递数据。
  2. 热 observable 是在测试开始之前,这observale 好像已经开始运行,如 subject ,是多播传递数据。

区分冷热 Observable 的关键是在于,订阅结束之后,数据源会不会被关闭销毁。

下面的案例中 cold 用来模拟一个会异步发出 a ,b ,c 三个值的 observable 。

it('generate the stream correctly', () => {
  testScheduler.run(helpers => {
    const { cold, expectObservable, expectSubscriptions } = helpers;
    const e1 =  cold('-a--b--c---|');
    const subs =     '^----------!';
    const expected = '-a-----c---|';
 
    expectObservable(e1.pipe(throttleTime(3, testScheduler))).toBe(expected);
  });
});

Cold 与 Hot 用法

既然Cold 与 Hot 可以用来模拟observable , 那么我们以 cold 为例,来说明这两种方法经常会用在哪些需求上。

我创建了一个 TestComponent 组件,doSomething 方法用来将值放大2倍,test 方法用来执行放大操作。我们测试这个代码会很简单,但是如何测试 catchError 异常代码呢?

export class TestComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }
  public test() {

    const a = of(1).pipe(switchMap(x => this.doSomething(x)),
      catchError(err => of(undefined)));
    return a;
  }
  public doSomething(x: number): Observable<number> {
    return of(2 * x);
  }

测试异常代码,mock doSomething 方法,然后返回cold("(#)") ,#代表异常,因此会抛出异常,然后被 catchError捕获到。捕获到之后会返回 of(undifined),因此可以断言返回 of(undifined), 判断是否正确。

  spyOn(component, "doSomething").and.returnValue(cold("(#)"))
  const t = component.test()
  expectObservable(t).toBe("(a|)", { a: undefined })

有的时候想让测试代码的中一个 Observable 变量(如 http observable )执行,是非常困难的,碰到类似的 observable 都可以用 cold 或者 hot 来模拟这个 observable 。

注意:本实验没有依赖第三方的测试库,使用的是 rxjs 自带的弹珠测试库,在angular 项目中使用可能会有异常,建议用 第三方的库。本实验的目的是为了学习弹珠语法,以及如何把弹珠测试应用在单元测试中。