简单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 值 | 0 | 1 | 2 | 3 | |||||
|---|---|---|---|---|---|---|---|---|---|
| 弹珠模拟 | - | - | a | - | b | - | c | - | d| |
| 时间 | 1ms | 2ms | 3ms | 4ms | 5ms | 6ms | 7ms | 8ms | 9ms |
如果上面的你弄明白之后,那么你就会知道,如何模拟 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
- 冷 observable 是只有当测试开始的时候这个 observable 订阅才刚开始发生 ,例如 of , from 等操作符。或者在 observable 内部 原生的使用 next 方法产生数据,是单播传递数据。
- 热 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 项目中使用可能会有异常,建议用 第三方的库。本实验的目的是为了学习弹珠语法,以及如何把弹珠测试应用在单元测试中。