如何替 RxJS 撰写测试 - 一般测试与弹珠图测试方法

今天我们来聊聊如何撰写测试程序来确保写出来的 RxJS 如我们所想的一般运作,也就是撰写测试程序码!撰写测试程序是软件开发中非常重要的一环,虽然不是所有程序码都一定要有对应的测试程序,但良好的测试程序却可以帮助我们撰写住更加稳固的程序码。

至於到底该怎麽测试 RxJS?撰写 RxJS 测试程序时又有什麽需要注意的呢?就让我们继续看下去吧!

范例专案说明

这边假设大家对於撰写前端测程序已经有一定理解和经验了,就不多说明基本的撰写测试方法了;接下来我们撰写的测试程序都是运行在 Karma + Jasmine 上,原始档如下:

https://github.com/wellwind/rxjs-marble-testing-demo

当然,想要用其他测试框架也是完全 ok 的喔!

只要专案下载下来後执行 npm install 安装相关套件,接着执行 npm run test 即可看到所有测试程序执行的结果。

专案结构

范例专案内多数都是些基本的设定如使用套件、TypeScript 设定等等,另外有两个重要的目录:

  • src:所有要测试的目标程序码所在的位置
  • test:所有针对测试目标所撰写的测试程序码

测试目标

在 RxJS 中,基本上有两件事情要测试,分别是

  • Observable 订阅得到的结果是否正确
  • Observable 资料流动的过程是否正确

在范例专案中,我们设计了几种 Observable,有些很好测试,有些则相对没那麽简单:

export const emitOne$ = of(1);
export const emitOneToFour$ = of(1, 2, 3, 4);
export const emitOntToFourPerSecond$ = timer(0, 1000).pipe(
  take(4)
);

另外我们也设计了一个 operator:

export const plusOne = () 
  => (source$: Observable<number>) 
	=> source$.pipe(map(value => value + 1));

这个 operator 内容很简单,将输入的资料加一而已,主要是拿来测试整个资料流是否能依照我们设计的 operator 流动。

另外,我们在前几天实战介绍「自动完成」功能时,有透过组合好几个 operators 来让资料查询不要那麽频繁,我们也将它抽成一个比较复杂一点的 operator 来测试看看:

export const debounceInput = () 
  => (source$: Observable<string>) 
    => source$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      filter(data => data.length >= 3)
    );

接着就让我们来看看两种不同的测试手法,一种很简单,但比较难测试各种情境;另外一种比较复杂,但可以应付几乎所有情境的测试。

在 subscribe callback 内进行测试

第一种方法很简单,这种手法在「同步 Observable」或是「非同步 Observable 但有明确结束时机点」两种状况时适合使用,也不用想太多,直接在 subscribe 的 callback 方法内测试结果即可。

测试程序码请参考范例专案内的 test/subscribe-test.spec.ts

单一事件值的 Observable 测试

针对 of(1) 这种只有一个事件值的测试程序可以很容易的直接在 subscribe 内进行验证:

// const emitOne$ = of(1);
it('测试单一个事件的 Observable', () => {
  emitOne$.subscribe(data => {
    expect(data).toEqual(1);
  });
});

非常简单吧!

多个事件值的 Observable 测试

那麽如果来源 Observable 有多个事件值呢?expect(data) 後面该怎麽写?这时我们可以在 subscribe 时将订阅值存起来,再测试结果:

// const emitOneToFour$ = of(1, 2, 3, 4);
it('测试多个事件的 Observable', () => {
  const actual: number[] = [];
  emitOneToFour$.subscribe(data => {
    actual.push(data);
  });
  expect(actual).toEqual([1, 2, 3, 4]);
});

上述程序第 4 行将每次事件资料存到 actual 阵列中,接着在第 7 行比较结果。由於整个 Observable 是「同步执行」的,因此可以确定所有事件都发送完毕,直到 Observable 结束後才进行比较。

非同步 Observable 测试

如果是「非同步执行」的 Observable 如 timer 处理呢?那就先要看测试框架是否支援非同步处理了,以 Jasmine 来说可以在测试 function 内传入一个 done 的 function,并在非同步程序中测试完毕後呼叫它代表非同步程序结束:

// const emitOntToFourPerSecond$ = timer(0, 1000).pipe(
//   take(4)
// );
it('测试非同步处理的 Observable', (done) => {
  const actual: number[] = [];
  emitOntToFourPerSecond$.subscribe({
    next: data => {
      actual.push(data);
    },
    complete: () => {
      expect(actual).toEqual([0, 1, 2, 3]);
      done();
    }
  });
});

上述程序第 8 行在 subscribe 中的 next() 将来源资料存入阵列,直到 Observable 结束後,在 complete() 内验证测试结果,并呼叫 done() 告知测试框架「非同步处理的程序码测试完毕」。

这种方式看起来合理,但上面程序码实际上有一些缺点

  • 来源 Observable 到结束会等待 3 秒钟,如果程序内有许多类似的 Observable 要测试,就会让整体测试时间拉长
  • 大部分测试框架在处理非同步时,都会给一个等待时间,逾时就会自动失败,以 Jasmine 来说预设是 5 秒钟,也就是 Observable 会需要超过 5 秒钟的话,必须另外设定拉长等待时间
  • 如果是一个不会结束的 Observable 呢?虽然我们也可以在程序中主动加上 take() operator 或想办法加上其他条件让它结束,但那就变成在写程序而不是写测试了,因此不是个推荐的解法

这些问题,在之後介绍「弹珠图测试」时会解决。

Operator 测试

如果要测试某个自订的 operator 是否如我们预期处理资料流,只需要准备一个来源 Observable 并搭配 pipe 将资料传入我们自订的 operator 即可:

// const plusOne = () 
//   => (source$: Observable<number>) 
//     => source$.pipe(map(value => value + 1));
it('使用 pipe 测试 operator', () => {
  of(1).pipe(
    plusOne()
  ).subscribe(data => {
    expect(data).toEqual(2);
  });
});

it('单独测试一个 operator', () => {
  const source$ = of(1);
  plusOne()(source$).subscribe(data => {
    expect(data).toEqual(2);
  });
});

有两种测试方式,其实是一样的概念,第一种是把它真的当作 operator,以操作 operator 的方式处理,因此第 5 行使用 of(1).pipe(...) 的方式,来确认资料流向及处理。

而第二种测试是把它当作一个 function,因为 operator 其实就是一个 function 而已,所以第 14 行我们呼叫这个 plusOne function 後,会得到一个参数和回传值都是 Observable 的 function,再把我们的来源 Observable 当参数传入,然後订阅测试结果。

Marble Testing 弹珠图测试

上一段落我们单纯的使用测试框架提供的功能来进行测试,已经足以应付不少情境了,但还是有些情境不是那麽适合,因此 RxJS 提供了一个 TestScheduler 测试工具,来协助我们以更直觉、图像的方式处理各种同步、非同步的 RxJS 程序码!

Scheduler 是用来「安排」事件发生时机点的工具,而 TestScheduler 就是其中一种,这种 Scheduler 可以帮助我们处理测试中遇到的各种问题。

关於 Scheduler,可以参考之前的文章「[RxJS] 认识 Scheduler

认识 TestScheduler

TestScheduler 是 RxJS 开发出来协助我们撰写测试程序的工具,它可以帮助我们:

  • 将所有「非同步执行」的 RxJS 程序码转换成「同步执行」
  • 建立假的 Observable,Hot 或 Cold Observable 都可以
  • 让我们使用弹珠图的视觉方式确认程序码运作结果

不过 TestScheduler 使用上也有些条件:「必须使用跟 timer 相关的 Observable 当作测试来源,如 timerintervaldelay 等等」,这类型的测试来源都是使用 asyncScheduler;而使用 Promise 或是其他非同步的 Scheduler 如 asapScheduleranimationFrameScheduler 则会变得比较不可靠,这是需要注意的一点。

当然,遇到这种状况时,还是可以走回原来 subsctibe callback 的测试方式

使用 TestScheduler 的基本流程

起手式:建立 TestScheduler

要建立 TestScheduler 很简单,因为它只是个类别,直接 new 它就好,而在建立时需要传入一个 callback function 用来决定如何比较两个物件是否相同。

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

上述程序我们产生了一个新的 TestScheduler,并在建立时指定比较两个物件的方法,以 Jasmine 的例子来说,我们使用 toEqual 来进行深层比较 (deep equality comparison),以确保物件内所有属性都完全相等。如果使用其他测试框架,则须要看该测试框架使用那种方式比较两个物件。

呼叫 run() 取得测试用的 helper

建立好 TestScheduler 後,我们需要呼叫该物件的 run() 方法,run() 方法内也是一个 callback function,在此 function 内会得到一个 helper 物件,这个物件可以帮助我们「以同步的方式测试非同步 Observable」,这个 helper 包含几个方法:

  • hot:依照指定的弹珠图建立一个 Hot Observable
  • cold:依照指定的蛋ˋ猪屠建立一个 Cold Observable
  • expectObservable(...).toBe(...):用来判断两条 Observable 是否结果相同
  • expectSubscription(...).toBe(...):用来判断「订阅」和「结束订阅」的结果是否符合预期
  • flush:用来立即完成一个 Observable,通常用不到,只要在很细的控制测试需要

认识测试用弹珠图

我们之前已经介绍过了「文字版弹珠图」,里面的符号在一般沟通时已经足够使用,但在测试时,需要更精准的知道事件发生的时间,因此让我们回顾一下基本的符号、在测试时的意义,以及之前没有用到的符号。

  • -:如同之前所介绍,它是一个时间的最小单位,在测试中我们称为一个 frame,通时它也虚拟地代表了 1 毫秒。
  • :空白符号,一样只是用来对齐使用,不会发生任何事情,也不代表时间
  • [0-9]+[ms|s|m]:代表经过了多少时间,毕竟一个 - 代表 1 毫秒,那一秒钟要画 1000 个 -,太辛苦了
  • [a-z0-9]:代表事件发生了,一个符号代表一个事件值发生了,也就是 next() 被呼叫了
  • ( ):用来群组同时间发生的资料,例如 (12) 并不是事件「十二」发生,而是同一个时间点 (frame) 发生了 12 两个事件
  • |:Observable 完成
  • #:Observable 发生错误
  • ^:代表订阅开始的时间点,专门用来测试订阅何时开始的
  • !:代表订阅结束的时间点,专门用来测试订阅何时结束的

上述符号在测试时,^! 在测试 Observable 资料流程时不可使用;而在测试订阅时机时,则可以使用 ^!、和时间相关的符号 1[0-9]+[ms|s|m],稍後看到实际程序会更加清楚。

实际画几个图来说明一下:

  • 使用时间符号取代 frame (-)
----------
(10ms)
// 上下两组发生时间一样长
  • 对齐加上时间符号
--- 1s ---
// 实际上用掉了 1006 个 frames
---    ---
// 实际只用掉了 6 个 frames
  • 事件发生加上时间符号
a 1s b
=> a 事件发生後,再等待 1000 个 frames 後发生 b 事件
  • 事件发生在同一个 frame
---  1   ---
---(abc)---
=> 用掉了 7 个 frame,事件 abc 都在同一个 frame 发生
  • 订阅发生时间
---1---2---3---4---5|
     ^-----------|
     => 实际上订阅的时间点

弹珠图测试范例

接着就让我们直接举例,用弹珠图来进行测试看看吧!我们先把 Observable 都写在测试程序里面,以便理解各种弹珠图测试方式。

测试程序码请参考范例专案内的 test/marble-test-basic.spec.ts

基本弹珠图测试

首先第一步,在每个要使用 TestScheduler 的测试之前,都需要建立一次 TestScheduler,因此我们用 beforeEach 来处理:

describe('使用 TestScheduler 测试', () => {
  let testScheduler: TestScheduler;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });    
});

接着只要在测试案例的 it 内,呼叫 testScheduler.run() 并执行测试即可,以下以 take operator 进行示范:

it('测试 take operator', () => {
  testScheduler.run(helpers => {
    const { cold, expectObservable, expectSubscriptions } = helpers;

    const sourceMarbleDiagram =  '---a---b---c---d---e---|';
    const expectedSubscription = '^----------!';
    const expectedResult =       '---a---b---(c|)';

    const sourceObservable = cold(sourceMarbleDiagram);
    const source$ = sourceObservable.pipe(take(3));

    expectObservable(source$)
      .toBe(expectedResult);
    expectSubscriptions(sourceObservable.subscriptions)
      .toBe(expectedSubscription);
  });
});
  • 第 2 行:呼叫 testScheduler.run 开始进行弹珠图测试
  • 第 3 行:将需要的程序从 helpers 中取出
  • 第 5 行:来源 Observable 的弹珠图
  • 第 6 行:实际测试的 Observable 订阅後预期的订阅时机点
  • 第 7 行:实际测试的 Observable 预期产出的弹珠图
  • 第 9 行:依照第 5 行的弹珠图,建立一个 Cold Observable
  • 第 10 行:建立实际要测试的 Observable,一般来说就是来源 Observable (第 9 行建立) 加上要测试的 operators (以这边的例子是 take(3))
  • 第 12 行,使用 expectObservable 测试 source$ 订阅後产生的资料流弹珠图是否符合预期
  • 第 14 行,使用 expectSubscriptions 测试 source$ 的订阅时机点 (sourceObservable.subscription)是否符合预期

这样就完成第一个弹珠图测试啦!之後要进行弹珠图测试时,可以用同样的模式,把要被测试的目标换掉 (以这边的例子是 source$),以及设定正确的弹珠图,就可以看到结果罗!

带入指定物件到弹珠图内

上述例子我们用 abc 当作事件值,但实际上我们的来源 Observable 不会只有这麽单纯的文字资料而已,很多时候都是物件的处理,这时候我们可以在使用 coldhot 建立 Observable 时给予一个对应事件名称当作属性的物件,当作来源资料;同时在 expectObservable 时用一样的方式给予物件,当作结果资料。先看个简单的例子:

it('测试 map operator (带入 value)', () => {
  testScheduler.run(helpers => {
    const { cold, expectObservable } = helpers;
    const sourceMarbleDiagram = '--a--b--c--d--|';
    const expectedResult =      '--w--x--y--z--|';

    const sourceObservable = cold(sourceMarbleDiagram, { a: 1, b: 2, c: 3, d: 4 });
    const source$ = sourceObservable.pipe(map(value => value + 1));
    expectObservable(source$).toBe(expectedResult, { w: 2, x: 3, y: 4, z: 5 });
  });
});
  • 第 4 行和第 5 行建立弹珠图,包含来源 Observable 和预期转换成新的 Observable 的弹珠图
  • 第 7 行使用 cold 建立 Cold Observable 时,除了给予弹珠图文字外,也传入一个物件,因此弹珠图上的事件 a 实际上发出的事件值会是物件内的数字 1,以此类推
  • 第 9 行在比较弹珠图结果时,toBe 传入预期的结果物件,因此弹珠图上的事件 w 实际上会发出的事件值会是物件内的数字 2,以此类推

看完简单的例子後,改成带入更复杂的物件看看,原理一样,就不多说明罗:

it('测试 map operator (带入更复杂的 value)', () => {
  testScheduler.run(helpers => {
    const { cold, expectObservable } = helpers;

    const input = {
      a: { name: 'Student A', score: 25 },
      b: { name: 'Student B', score: 49 },
      c: { name: 'Student C', score: 100 },
      d: { name: 'Student D', score: 0 }
    };
    const expected = {
      w: { name: 'Student A', score: 50 },
      x: { name: 'Student B', score: 70 },
      y: { name: 'Student C', score: 100 },
      z: { name: 'Student D', score: 0 }
    };

    const sourceMarbleDiagram = '--a--b--c--d--|';
    const expectedResult =      '--w--x--y--z--|';

    const sourceObservable = cold(sourceMarbleDiagram, input);

    const source$ = sourceObservable.pipe(
      map(student => ({ ...student, score: Math.sqrt(student.score) * 10 }))
    );
    expectObservable(source$).toBe(expectedResult, expected);
  });
});

测试长时间的 Observable

使用 testScheuler 除了可以画图用视觉画理解测试外,另一个最大的方便就是一切都变成同步执行的,尽管 Observable 是每秒发生一次,在 testScheduler 内也只是虚拟时间,不会等待那麽长的时间,而弹珠图中也可以直接用时间单位表达发生多久,如下:

it('测试时间 time frame', () => {
  testScheduler.run(helpers => {
    const { cold, expectObservable } = helpers;

    const sourceMarbleDiagram = '(123|)';
    const expectedResult =      '--- 7ms 1 9ms 2 9ms (3|)';

    const sourceObservable = cold(sourceMarbleDiagram);
    const source$ = sourceObservable.pipe(
      concatMap(value => of(value).pipe(delay(10)))
    );

    expectObservable(source$).toBe(expectedResult);
  });
});

这里重点测试是 source$ 内的 delay(10),透过 concatMap 让原始 Observable 的每个值都延迟 10ms 发生,然後串在一起。

expectedResult 里面 --- 7ms 实际上就是 10 毫秒,直接写成 10 ms 也可以,意义完全一样;;而整体 Observable 弹珠图则是 1 9ms 2 9ms (3|),值得注意的是每次事件发生的那个当下也代表了一个 frame,因此是 1 9ms 而不是 1 10ms,而最後事件值 3 发生後会直接结束,因此使用 ( ) 包起来, (3|) 代表两个事件 (3| 结束) 是在同一个 frame 发生的。

就算是一个不会结束的 Observable,弹珠图画出来也只是没有 | 符号而已罗。

测试 Hot Observable

Cold Observable 在每次订阅时会重新跑完整个资料流,而 Hot Observable 则是一个资料流,在不同时间点订阅可能会得到不同的结果,因此 Hot Observable 测试时还有一个重点,就是不同时间订阅的结果,听起来很麻烦,但一画成弹珠图就简单多了!

it('测试 Hot Observable', () => {
  testScheduler.run((helpers) => {
    const { hot, expectObservable } = helpers;

    const sourceMarbleDiagram = '--1--2--3--4--5--6--7--8';
    const subscription1 =       '-------^-------!';
    const subscription2 =       '-----------^-----!';
    const expectedResult1 =     '--------3--4--5-';
    const expectedResult2 =     '-----------4--5---';

    const sourceObservable = hot(sourceMarbleDiagram);

    expectObservable(
      sourceObservable, 
      subscription1
    )
      .toBe(expectedResult1);
    expectObservable(
      sourceObservable, 
      subscription2
    )
      .toBe(expectedResult2);
  });
});

上面程序我们只订了两个订阅时机 (subscription1subscription2),从弹珠图就可以看到时机点的不同,以及画出两种预期得到资料的结果,在处理 Hot Observable 时,可以在 expectObservable 中给予订阅的时机,藉此来比较该订阅时机点下,得到的弹珠图是否与预期相同。

使用弹珠图测试实际的 Observables 与 Operators

有了这些观念後,就让我们实际测试看看一开始写的那些程序吧!

测试程序码可以参考范例专案内的 src/marble-test-for-observables.spec.ts 以及 src/marble-test-debounce-input.spec.ts 两个档案,里面有各种写法,有了前面的观念後要写出类似的测试程序应该不困难,这里只挑出一些重点。

基本弹珠图测试

与之前一样,只是来源 Observable 是我们真正写好的程序码:

it('使用弹珠图测试单一个事件的 Observable', () => {
  testScheduler.run((helpers) => {
    const { expectObservable } = helpers;

    // 1 会被当事件字串,因此不能这样写
    // const expectedResult = (1|);
    const expected = '(a|)';
    expectObservable(emitOne$).toBe(expected, { a: 1 });
  });
});

这里有一个值得注意的小地方,由於弹珠图内的事件预设都是字串,因此 of(1) 虽然一般可以想成弹珠图 (1|),但在测试时 1 会被当作字串,而跟结果比较不符合,所以正确的写法是给一个字母如 a 当作事件点,然後把物件传入来指定该是件时间点的事件值。

测试多个事件的 Observable

可以画弹珠图之後,一切就变得简单啦!

it('使用弹珠图测试多个事件的 Observable', () => {
  testScheduler.run(helpers => {
    const { expectObservable } = helpers;

    const expected = '(abcd|)';
    expectObservable(emitOneToFour$)
      .toBe(expected, { a: 1, b: 2, c: 3, d: 4 });
  });
});

一样要注意的是资料型态问题。

测试非同步的 Observable

要测试非同步 Observable 也不困难,在弹珠图上标记经过时间即可:

it('使用弹珠图测试非同步处理的 Observable', () => {
  testScheduler.run((helpers) => {
    const { expectObservable } = helpers;

    // 因为事件本身占一个 frame,所以用 999ms
    const expected = 'a 999ms b 999ms c 999ms (d|)';
    expectObservable(emitOntToFourPerSecond$)
      .toBe(expected, { a: 0, b: 1, c: 2, d: 3 });
  });
});

跟之前提过的一样,事件发生点本身就占一个 frame,这是需要多注意的地方。

测试复杂的 Observable

最後要来测试前面提到旦没测试过的 debounceInput() 啦!这个 debounceInput() 是三个 operators 组合起来的,所以至少根据每个 operator 特性会撰写一组测试案例才对,同时这些测试案例的结果也必须符合组合起来的结果。

首先是最简单的「文字长度大於等於 3」也就是 filter(data => data.length >= 3) 的条件:

it('文字长度大於等於 3 才允许事件发生', () => {
  testScheduler.run((helpers) => {
    const { cold, expectObservable } = helpers;
    const input = {
      a: 'rxjs',
      b: 'rx',
    };
    const expectedOutput = {
      x: 'rxjs',
    };

    // b 事件的内容不到 3 个字,因此没有事件发生
    const sourceMarbleDiagram = 'a 300ms   100ms b';
    const expectedResult =      '  300ms x 100ms  ';

    const sourceObservable = cold(sourceMarbleDiagram, input);
    const source$ = sourceObservable.pipe(debounceInput());
    expectObservable(source$)
      .toBe(expectedResult, expectedOutput);
  });
});

input 代表每次事件发生时输入的内容,可以搭配 sourceMarbleDiagram 弹珠图一起看,而 expectedOutput 则搭配 expectedResult 弹珠图忆起看,在事件 a 时间点时,输入内容超过 3 个字,因此预期的 Observable 会得到这个资料,而事件 b 时间点指输入 2 个字,因此在新的 Observable 上没有事件发生。同时考量到整个 operator 有一个 debounceTime(300),因此事件 a 发生後等待 300ms 没有新事件才会在新的 Observable 上发生事件 x

这里的 x 实际上就是 a 事件,因此我们也可以把 expectedResultx 直接取代成 a,比较时可以都传入 input 会更好理解这个弹珠图,这里只是示范带入不同物件的方法。

接着测试 debounceTime(300) 的行为:

it('300ms 内没有的输入才允许事件发生', () => {
  testScheduler.run((helpers) => {
    const { cold, expectObservable } = helpers;
    const input = {
      a: 'rxjs-demo',
      b: 'rxjs-test',
      c: 'rxjs',
    };

    // a--b 後等待 100ms 继续输入文字 (事件 c),因为没超过 300ms 所以没有新事件
    // 之後 300ms 没有新的输入,将最後资料当作事件发送
    const sourceMarbleDiagram = 'a--b 100ms c';
    const expectedResult =      '---- 100ms 300ms c';

    const sourceObservable = cold(sourceMarbleDiagram, input);
    const source$ = sourceObservable.pipe(debounceInput());
    expectObservable(source$).toBe(expectedResult, input);
  });
});

考量到 filter(data => data.length >= 3) 的条件,我们将事件内容都设定超过 3 个字,而事件 abc 之间的间隔都没有超过设定的 300ms 因此过程中不会有新的事件,直到事件 c 发生後 300ms 没有新事件,才在新的 Observable 发生来源 Observable 的最後一个事件值 c

最後测试 distinceUntilChanged 的行为:

it('事件值跟上次相同时,不允许本次事件发生', () => {
  testScheduler.run((helpers) => {
    const { cold, expectObservable } = helpers;

    const input = {
      a: 'rxj',
      b: 'rxjs',
      c: 'rxjs',
      d: 'rxj',
    };

    // 由於 b 事件跟 c 事件的内容一样,因此事件不会发生
    // 由於 c 事件跟 d 事件的内容不同,因此事件继续发生
    // 若使用 distinct 则会因为 a 和 d 事件一样而不发生新事件
    const sourceMarbleDiagram = 'a 300ms   b 300ms   c 300ms d';
    const expectedResult =      '  300ms a   300ms b - 300ms   300ms d';

    const sourceObservable = cold(sourceMarbleDiagram, input);
    const source$ = sourceObservable.pipe(debounceInput());
    expectObservable(source$).toBe(expectedResult, input);
  });
});

一样的需要考量到 debounce(300)filter(data => data.length >= 3) 的问题;事件 a 发生後 300ms 发生在新的 Observable 上,事件 b 也是;而事件 c 和事件 b 内容相同,因此不会发生在新的 Observable 上。

本日小结

今天我们学习到两种测试 RxJS 的方法

  • 直接在 subscribe callback 内验证:直觉简单,但比较不能应付复杂情境,非同步程序的测试可能会拉长整体测试时间
  • 使用弹珠图测试 (Marble Testing):使用 TestScheduler;前置步骤比较多,但可以画出弹珠图来以视觉画的方式进行测试,同时可以把所以非同步程序都当作同步程序来测试,效能会更好

如果 Observable 真的很简单,可以考虑使用 subscribe callback 的方法就好,如果遇到比较复杂的状况,RxJS 都帮我们想好了,使用 TestScheduler 可以帮助我们用很好理解的方式撰写测试程序罗!

相关资源

完赛感言

终於!把我觉得 RxJS 想介绍的内容都写完了!!每次写铁人赛都有一种很深的感觉:「自己才是学到最多的那个人」,这种感觉很棒!!当然,也希望这 35 篇内容可以帮助大家由浅入深的理解 RxJS,同时体会到它的强大、便利以及可靠。虽然 RxJS 真的是一个比较抽象的东西,但只要花点时间多多练习这些内容,相信一定可以帮助大家打通 RxJS 任督二脉,摇身一变成高手啦!

RxJS 还有很多 operators 没机会介绍到,之後我会再花些时间整理这些 operators,以及补上更多实战练习,这部分就敬请期待罗。


<<:  强型闯入DenoLand[33] - Web API 正式完成!

>>:  在阵列找最大值和最小值

【D16】熟悉新厨具:Scanner

前言 串连行情的部分OK了,我们再回到盘後资讯,这是要使用的是Scanners。 参考网站:Stoc...

[Day-22] 呼叫自订函式小练习

上次练习完了自订函式的基础 今天就要来练习相关题目罗~ 题目: 1.计算:键盘输入任一整数a,计算2...

Day 29 - 开发流程(上) 瀑布式(Waterfall Model) & 敏捷式(Agile Model)

终於快结束30天的挑战了,专案开发的知识点除了环境安装、技术学习以外,还有一个重点知识,那就是专案的...

19. 好的面试官不只要有能力,更要懂得同理

前言 这篇蛮适合给刚要成为interview的人,帮助你思考要怎麽同理与善待面试者。 演讲总结 整...

Day 32 | 常见 Livewire 问题:与 Controller 兼容的几种方式

使用 Livewire 之後,在 Layout 上会改用 component 的 $slot 方式来...