• 测试
    • 符号

    测试

    异步代码的测试通常很棘手。异步代码可能毫秒间完成,也能几分钟才完成。所以你需要一种方法来完全模仿它,就像你在 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) 本质上说这两个值在同一个时帧上发出,等等。现在,你希望了解符号的工作原理和基础知识,我强烈建议你编写自己的测试来直到完全掌握它,并学习本章开头提到的官方文档页面上提供的其他符号。

    快乐测试