目录

烹饪宝典 - 测试

优质
小牛编辑
141浏览
2023-12-01

异步代码的测试通常很棘手。异步代码可能毫秒间完成,也能几分钟才完成。所以你需要一种方法来完全模仿它,就像你在 jasmine 中所做的一样。

  1. spyOn(service,'method').and.callFake(() => {
  2. return {
  3. then : function(resolve, reject){
  4. resolve('some data')
  5. }
  6. }
  7. })

或简写版本:

  1. spyOn(service,'method').and.callFake(q.when('some data'))

要点是你尝试避免时间相关的东西。RxJS 是有历史的,RxJS 4 提供了一种方法,这种方法使用 TestScheduler 和它的内部时钟,这使你能够增强对时间的把控。这种方法有两种风格:

方法 1

  1. let testScheduler = new TestScheduler();
  2. // 我的演示
  3. let stream$ = Rx.Observable
  4. .interval(1000, testScheduler)
  5. .take(5);
  6. // 设置测试
  7. let result;
  8. stream$.subscribe(data => result = data);
  9. testScheduler.advanceBy(1000);
  10. assert( result === 1 )
  11. testScheduler.advanceBy(1000);
  12. ... 再次断言, 等等..

这种方法很容易理解。第二种方法使用热的 observable 和 startSchedule() 方法,看起来像这样:

  1. // 设置输出数据
  2. var input = scheduler.createHotObservable(
  3. onNext(100, 'abc'),
  4. onNext(200, 'def'),
  5. onNext(250, 'ghi'),
  6. onNext(300, 'pqr'),
  7. onNext(450, 'xyz'),
  8. onCompleted(500)
  9. );
  10. // 应用操作符
  11. var results = scheduler.startScheduler(
  12. function () {
  13. return input.buffer(function () {
  14. return input.debounce(100, scheduler);
  15. })
  16. .map(function (b) {
  17. return b.join(',');
  18. });
  19. },
  20. {
  21. created: 50,
  22. subscribed: 150,
  23. disposed: 600
  24. }
  25. );
  26. // 断言
  27. collectionAssert.assertEqual(results.messages, [
  28. onNext(400, 'def,ghi,pqr'),
  29. onNext(500, 'xyz'),
  30. onCompleted(500)
  31. ]);

IMO 读起来有些费劲,但你仍然可以得到这个想法,你控制着时间,因为有 TestScheduler 来规定时间有多快。

这一切都是在 RxJS 4 进行的,在 RxJS 5 中有一些改变。我应该说,我要写下来的是一个大体的方向和一个前进的目标,所以这一章将会更新。我们开始吧。

在 RxJS 5 中使用的是叫做“弹珠测试(Marble Testing)”的东西。是的,这和弹珠图是有关系的,弹珠图就是用图形符号表达预期输入和实际输出。

我第一次看官方文档的编写弹珠测试页面的时候,我完全是懵的,不知道应该怎么做。但是当我自己写了一些测试后,我得出一个结论,这是一种十分优雅的方法。

所以我会通过展示代码来进行说明:

  1. // 设置
  2. const lhsMarble = '-x-y-z';
  3. const expected = '-x-y-z';
  4. const expectedMap = {
  5. x: 1,
  6. y: 2,
  7. z : 3
  8. };
  9. const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
  10. const myAlgorithm = ( lhs ) =>
  11. Rx.Observable
  12. .from( lhs );
  13. const actual$ = myAlgorithm( lhs$ );
  14. // 断言
  15. testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
  16. testScheduler.flush();

我们分解来看

设置

  1. const lhsMarble = '-x-y-z';
  2. const expected = '-x-y-z';
  3. const expectedMap = {
  4. x: 1,
  5. y: 2,
  6. z : 3
  7. };
  8. const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });

我们基本上为 TestScheduler 上存在的方法 createHotObservable() 创建了一种模式指令 -x-y-zcreateHotObservable() 是一个工厂方法,为我们做了大量的事情。作为对比,自己实现这个方法的话,在这个案例中相对应的应该像这样:

  1. let stream$ = Rx.Observable.create(observer => {
  2. observer.next(1);
  3. observer.next(2);
  4. observer.next(3);
  5. })

我们不自己做的原因是我们想要 TestScheduler 来完成,这样时间就会根据其内部时钟流转。还要注意,我们定义一个预期模式和一个预期映射:

  1. const expected = '-x-y-z';
  2. const expectedMap = {
  3. x: 1,
  4. y: 2,
  5. z : 3
  6. }

那是我们需要的设置,但是要想测试运行起来还需要 flush,这样 TestScheduler 内部才可以触发 HotObservable 并运行断言。看下 createHotObservable() 方法的源码,我们发现它解析了我们给定的弹珠模式并添加到列表之中:

  1. // 摘自 createHotObservable
  2. var messages = TestScheduler.parseMarbles(marbles, values, error);
  3. var subject = new HotObservable_1.HotObservable(messages, this);
  4. this.hotObservables.push(subject);
  5. return subject;

接下来是两个步骤的断言 1) expectObservable() 2) flush()

预期的调用差不多就是设置了 HotObservable 的订阅

  1. // 摘自 expectObservable()
  2. this.schedule(function () {
  3. subscription = observable.subscribe(function (x) {
  4. var value = x;
  5. // 支持高阶 Observable
  6. if (x instanceof Observable_1.Observable) {
  7. value = _this.materializeInnerObservable(value, _this.frame);
  8. }
  9. actual.push({ frame: _this.frame, notification: Notification_1.Notification.createNext(value) });
  10. }, function (err) {
  11. actual.push({ frame: _this.frame, notification: Notification_1.Notification.createError(err) });
  12. }, function () {
  13. actual.push({ frame: _this.frame, notification: Notification_1.Notification.createComplete() });
  14. });
  15. }, 0);

通过定义一个内部的 schedule() 方法并调用它。断言的第二部分是断言本身:

  1. // 摘自 flush()
  2. while (readyFlushTests.length > 0) {
  3. var test = readyFlushTests.shift();
  4. this.assertDeepEqual(test.actual, test.expected);
  5. }

最后将两个列表,actualexpect 进行比较。它执行的是深层次的比较并验证两件事,即数据发生在正确的时帧上和时帧上的值是正确的。所以这两个列表都包含如下所示的对象:

  1. {
  2. frame : [some number],
  3. notification : { value : [your value] }
  4. }

这些属性都必须相等,那么断言才为真。

看起来没那么血腥吧?

符号

我还没有真正解释过我们所看到的:

  1. -a-b-c

但它实际上是有含义的。- 意味着流逝的时帧。a 只是个符号。所以你写了多少个实际的和预期的 - 是很重要的,因为它们需要匹配预期。来看下另一个测试,这样你能理解它并在这个过程中引入更多的符号:

  1. const lhsMarble = '-x-y-z';
  2. const expected = '---y-';
  3. const expectedMap = {
  4. x: 1,
  5. y: 2,
  6. z : 3
  7. };
  8. const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
  9. const myAlgorithm = ( lhs ) =>
  10. Rx.Observable
  11. .from( lhs )
  12. .filter(x => x % 2 === 0 );
  13. const actual$ = myAlgorithm( lhs$ );
  14. // 断言
  15. testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
  16. testScheduler.flush();

在这个案例中,我们的演示包含了一个 filter() 操作。这意味着不会发出1,2,3,只有2会被发出。看下我们的输入模式:

  1. '-x-y-z'

和预期模式

  1. `---y-`

在这你可以清楚的认识到 - 是不重要的。每个你写的符号 -x 等都发生在某个时间点,所以在这个案例中,由于 filter() 方法 xz 不会发生,这意味着我们只需在结果输出中用 - 来替换它们

  1. -x-y

变成

  1. ---y

因为 x 不会发生。

当然还有其他操作符,它们也有很意思,可以让我们定义一些东西,比如错误。错误用 # 来表示,下面就是一个包含错误测试的示例:

  1. const lhsMarble = '-#';
  2. const expected = '#';
  3. const expectedMap = {
  4. };
  5. const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
  6. const myAlgorithm = ( lhs$ ) =>
  7. Rx.Observable
  8. .from( lhs );
  9. const actual$ = myAlgorithm( Rx.Observable.throw('error') );
  10. // 断言
  11. testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
  12. testScheduler.flush();

还有另外一个符号 | 表示流的完成:

  1. const lhsMarble = '-a-b-c-|';
  2. const expected = '-a-b-c-|';
  3. const expectedMap = {
  4. a : 1,
  5. b : 2,
  6. c : 3
  7. };
  8. const myAlgorithm = ( lhs ) =>
  9. Rx.Observable
  10. .from( lhs );
  11. const lhs$ = testScheduler.createHotObservable(lhsMarble, { a: 1, b: 2, c :3 });
  12. const actual$ = lhs$;
  13. testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
  14. testScheduler.flush();

还有更多的符号,像 (ab) 本质上说这两个值在同一个时帧上发出,等等。现在,你希望了解符号的工作原理和基础知识,我强烈建议你编写自己的测试来直到完全掌握它,并学习本章开头提到的官方文档页面上提供的其他符号。

快乐测试