2

上篇文章中我们讲述了如何在angular中的使用弹珠测试来模拟http异步请求。

使用弹珠测试的方法虽然很官方很正统,但由于其调度器的生效周期的设置原因,若要在单元测试中结合fixture.detectChanges();在某个测试用例完毕后继续与组件进行交互,则会发生No test scheduler initialized!错语。

在单元测试中手动地与组件进行交互虽不正规,但该方案无疑会让新成员感受到单元测试友好的一面,降低对单元测试的心理排斥。

sample

比如当前有一组件的功能是当用户在input中输入相应的内容时,首先使用500ms防抖阻止冗余的请求,接着进行后台的模拟异步请求:

组件相关代码:

    // 当输入发生变化时,500ms稳定后重新由后台拉取数据
    this.input.valueChanges.pipe(
      debounceTime(500),
      distinctUntilChanged(),
      switchMap(value => {
        this.tagName = value;
        
        // 请求后台
        return this.tagService.findTop20ByCourseAndNameLike(this.state.courseId, value);
      })
    ).subscribe((tags: Array<Tag>) => {
      this.tags = tags;
    });

使用弹珠测试的服务层tagService相关代码:

  /**
   * 模拟真实后台,异步返回数据
   */
  public findTop20ByCourseAndNameLike(courseId: number, name: string): Observable<Array<Tag>> {
    Assert.notNull(courseId, '课程ID未定义');

    const tags = new Array<Tag>();
    for (let i = 0; i < 20; i++) {
      tags.push(new Tag({
        id: randomNumber(),
        name: randomString('name'),
        course: new Course({id: randomNumber()})
      }));
    }
    
    // 使用弹珠测试返回tags
    return cold('---(x|)', {x: tags});
  }

基于上述代码,在单元测试中若仅把单元测试的目标定格为单元测试,则不会有任何问题。

但如果我们将单元测试的目标扩展为:不启动后台的前提下,单独快速开发前台相关组件。那么弹珠测试将无法满足该需求:

当我们看到单元测试生成的被测试组件时,总想点击一些功能性的按钮,用“眼睛”来确认自己的程序是正确的 ---- 尽管我们知道这并不是单元测试的正确用法,但却无法控制住自己的好奇心。
  fit('should create', fakeAsync(() => {
    expect(component).toBeTruthy();
    
    // 自动检测组件变化并重新组件渲染
    fixture.autoDetectChanges();
  }));

基于弹珠测试在与被测试组件进行人为交互则会发生错误:

s1.gif

如上所示,我们本期待输入某些字符后,cold()方法能够在一定时间后返回模拟后台返回的数据,但却得到了No test scheduler initialized!的错误。

个人猜想这可能是由于jasmine-marbles设置了有效上下文作用域造成的。

rxjs-marbles

rxjs-marbles提供了一种解决方法:让我们可以在不使用补丁的情况下,使用tick()方法来模拟普通时钟以及RxJS调度器的时间推进。

这使得我们可以抛弃弹珠测试而改用delay的方法来完成异步模拟请求,更重要的:像debounceTime等一些时间相关的操作符也能够被轻松的模拟。

从而达到了:1. 在单元测试中以够实现模拟时间推进,从而保障单元测试的正常运行。2. 单元测试结束后delay()debounceTime等操作符并不依赖于测试环境,从而保障了在人为与组件进行交互时,仍然能够接收到模拟后台的异步数据。

安装

npm i rxjs-marbles --save-dev

使用

tagService

// 删除原弹珠测试
- return cold('---(x|)', {x: tags});

// 使用of + delay的方法来模拟异步延迟请求
+ return of(tags).pipe(delay(500));

单元测试验证

使用tick进行模拟时钟推进:

import {fakeSchedulers} from 'rxjs-marbles/jasmine/angular';

  // fakeSchedulers使我们可以使用tick()来模拟rxjs中的时钟推进
  it('input change', fakeSchedulers(() => {
    component.tags = [];
    component.input.setValue('hello');

    // 模拟推进第一个防抖
    tick(500);

    // 断言正确的推进了防抖操作符
    expect(component.tagName).toEqual('hello');

    // 断言由于异步delay的作用,组件中的tags未发生变化
    expect(component.tags.length).toEqual(0);

    // 模拟推进tagService中的delay
    tick(500);

    // 断言成功推进了delay操作符
    expect(component.tags.length).toBeGreaterThan(1);
  }));

组件交互效果

此时,我们借助此单元测试生成的组件进行一些交互性的测试

s2.gif

总结

弹珠测试仅适用于单元测试的测试环境,一旦单元测试结束弹珠的相关代码便会失效。从而使得无法在单元测试中借助弹珠测试对组件进行手动的查看。在初次接触单元测试时,无法更加直观的感受到组件相关功能。
rxjs-marbles使tick()方法能够作用于RxJS的相关时间操作符上。当单元测试结束后,被测组件仍然可以在相关时间操作符的支持下完成交互功能,这使我们能够在单元测试中更加直观的来观察组件的动作。

本文作者:河北工业大业梦云智开发团队 潘杰

潘杰
3.1k 声望239 粉丝