烹饪宝典 - 测试
异步代码的测试通常很棘手。异步代码可能毫秒间完成,也能几分钟才完成。所以你需要一种方法来完全模仿它,就像你在 jasmine 中所做的一样。
spyOn(service,'method').and.callFake(() => {
return {
then : function(resolve, reject){
resolve('some data')
}
}
})
或简写版本:
spyOn(service,'method').and.callFake(q.when('some data'))
要点是你尝试避免时间相关的东西。RxJS 是有历史的,RxJS 4
提供了一种方法,这种方法使用 TestScheduler 和它的内部时钟,这使你能够增强对时间的把控。这种方法有两种风格:
方法 1
let testScheduler = new TestScheduler();
// 我的演示
let stream$ = Rx.Observable
.interval(1000, testScheduler)
.take(5);
// 设置测试
let result;
stream$.subscribe(data => result = data);
testScheduler.advanceBy(1000);
assert( result === 1 )
testScheduler.advanceBy(1000);
... 再次断言, 等等..
这种方法很容易理解。第二种方法使用热的 observable 和 startSchedule()
方法,看起来像这样:
// 设置输出数据
var input = scheduler.createHotObservable(
onNext(100, 'abc'),
onNext(200, 'def'),
onNext(250, 'ghi'),
onNext(300, 'pqr'),
onNext(450, 'xyz'),
onCompleted(500)
);
// 应用操作符
var results = scheduler.startScheduler(
function () {
return input.buffer(function () {
return input.debounce(100, scheduler);
})
.map(function (b) {
return b.join(',');
});
},
{
created: 50,
subscribed: 150,
disposed: 600
}
);
// 断言
collectionAssert.assertEqual(results.messages, [
onNext(400, 'def,ghi,pqr'),
onNext(500, 'xyz'),
onCompleted(500)
]);
IMO 读起来有些费劲,但你仍然可以得到这个想法,你控制着时间,因为有 TestScheduler
来规定时间有多快。
这一切都是在 RxJS 4 进行的,在 RxJS 5 中有一些改变。我应该说,我要写下来的是一个大体的方向和一个前进的目标,所以这一章将会更新。我们开始吧。
在 RxJS 5 中使用的是叫做“弹珠测试(Marble Testing)”的东西。是的,这和弹珠图是有关系的,弹珠图就是用图形符号表达预期输入和实际输出。
我第一次看官方文档的编写弹珠测试页面的时候,我完全是懵的,不知道应该怎么做。但是当我自己写了一些测试后,我得出一个结论,这是一种十分优雅的方法。
所以我会通过展示代码来进行说明:
// 设置
const lhsMarble = '-x-y-z';
const expected = '-x-y-z';
const expectedMap = {
x: 1,
y: 2,
z : 3
};
const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
const myAlgorithm = ( lhs ) =>
Rx.Observable
.from( lhs );
const actual$ = myAlgorithm( lhs$ );
// 断言
testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
testScheduler.flush();
我们分解来看
设置
const lhsMarble = '-x-y-z';
const expected = '-x-y-z';
const expectedMap = {
x: 1,
y: 2,
z : 3
};
const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
我们基本上为 TestScheduler 上存在的方法 createHotObservable()
创建了一种模式指令 -x-y-z
。createHotObservable()
是一个工厂方法,为我们做了大量的事情。作为对比,自己实现这个方法的话,在这个案例中相对应的应该像这样:
let stream$ = Rx.Observable.create(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
})
我们不自己做的原因是我们想要 TestScheduler
来完成,这样时间就会根据其内部时钟流转。还要注意,我们定义一个预期模式和一个预期映射:
const expected = '-x-y-z';
const expectedMap = {
x: 1,
y: 2,
z : 3
}
那是我们需要的设置,但是要想测试运行起来还需要 flush
,这样 TestScheduler
内部才可以触发 HotObservable 并运行断言。看下 createHotObservable()
方法的源码,我们发现它解析了我们给定的弹珠模式并添加到列表之中:
// 摘自 createHotObservable
var messages = TestScheduler.parseMarbles(marbles, values, error);
var subject = new HotObservable_1.HotObservable(messages, this);
this.hotObservables.push(subject);
return subject;
接下来是两个步骤的断言 1) expectObservable() 2) flush()
预期的调用差不多就是设置了 HotObservable 的订阅
// 摘自 expectObservable()
this.schedule(function () {
subscription = observable.subscribe(function (x) {
var value = x;
// 支持高阶 Observable
if (x instanceof Observable_1.Observable) {
value = _this.materializeInnerObservable(value, _this.frame);
}
actual.push({ frame: _this.frame, notification: Notification_1.Notification.createNext(value) });
}, function (err) {
actual.push({ frame: _this.frame, notification: Notification_1.Notification.createError(err) });
}, function () {
actual.push({ frame: _this.frame, notification: Notification_1.Notification.createComplete() });
});
}, 0);
通过定义一个内部的 schedule()
方法并调用它。断言的第二部分是断言本身:
// 摘自 flush()
while (readyFlushTests.length > 0) {
var test = readyFlushTests.shift();
this.assertDeepEqual(test.actual, test.expected);
}
最后将两个列表,actual
和 expect
进行比较。它执行的是深层次的比较并验证两件事,即数据发生在正确的时帧上和时帧上的值是正确的。所以这两个列表都包含如下所示的对象:
{
frame : [some number],
notification : { value : [your value] }
}
这些属性都必须相等,那么断言才为真。
看起来没那么血腥吧?
符号
我还没有真正解释过我们所看到的:
-a-b-c
但它实际上是有含义的。-
意味着流逝的时帧。a
只是个符号。所以你写了多少个实际的和预期的 -
是很重要的,因为它们需要匹配预期。来看下另一个测试,这样你能理解它并在这个过程中引入更多的符号:
const lhsMarble = '-x-y-z';
const expected = '---y-';
const expectedMap = {
x: 1,
y: 2,
z : 3
};
const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
const myAlgorithm = ( lhs ) =>
Rx.Observable
.from( lhs )
.filter(x => x % 2 === 0 );
const actual$ = myAlgorithm( lhs$ );
// 断言
testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
testScheduler.flush();
在这个案例中,我们的演示包含了一个 filter()
操作。这意味着不会发出1,2,3,只有2会被发出。看下我们的输入模式:
'-x-y-z'
和预期模式
`---y-`
在这你可以清楚的认识到 -
是不重要的。每个你写的符号 -
或 x
等都发生在某个时间点,所以在这个案例中,由于 filter()
方法 x
和 z
不会发生,这意味着我们只需在结果输出中用 -
来替换它们
-x-y
变成
---y
因为 x
不会发生。
当然还有其他操作符,它们也有很意思,可以让我们定义一些东西,比如错误。错误用 #
来表示,下面就是一个包含错误测试的示例:
const lhsMarble = '-#';
const expected = '#';
const expectedMap = {
};
const lhs$ = testScheduler.createHotObservable(lhsMarble, { x: 1, y: 2, z :3 });
const myAlgorithm = ( lhs$ ) =>
Rx.Observable
.from( lhs );
const actual$ = myAlgorithm( Rx.Observable.throw('error') );
// 断言
testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
testScheduler.flush();
还有另外一个符号 |
表示流的完成:
const lhsMarble = '-a-b-c-|';
const expected = '-a-b-c-|';
const expectedMap = {
a : 1,
b : 2,
c : 3
};
const myAlgorithm = ( lhs ) =>
Rx.Observable
.from( lhs );
const lhs$ = testScheduler.createHotObservable(lhsMarble, { a: 1, b: 2, c :3 });
const actual$ = lhs$;
testScheduler.expectObservable(actual$).toBe(expected, expectedMap);
testScheduler.flush();
还有更多的符号,像 (ab)
本质上说这两个值在同一个时帧上发出,等等。现在,你希望了解符号的工作原理和基础知识,我强烈建议你编写自己的测试来直到完全掌握它,并学习本章开头提到的官方文档页面上提供的其他符号。
快乐测试