【问题标题】:Unit Test RxJS Observable.timer using typescript, karma and jasmine使用 typescript、karma 和 jasmine 对 RxJS Observable.timer 进行单元测试
【发布时间】:2017-01-03 04:37:04
【问题描述】:

您好,我对 Angular2、Karma 和 Jasmine 还比较陌生。目前我正在使用 Angular 2 RC4 Jasmine 2.4.x 我有一个 Angular 2 服务,它定期调用这样的 http 服务:

getDataFromDb() { return Observable.timer(0, 2000).flatMap(() => {
        return this.http.get(this.backendUrl)
            .map(this.extractData)
            .catch(this.handleError);
    });
}

现在我想测试功能。出于测试目的,我刚刚在没有 Observable.timer 的单独函数上测试了“http.get”:

const mockHttpProvider = {
    deps: [MockBackend, BaseRequestOptions],
    useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => {
        return new Http(backend, defaultOptions);
    }
}

describe('data.service test suite', () => {
    var dataFromDbExpected: any;

    beforeEachProviders(() => {
        return [
            DataService,
            MockBackend,
            BaseRequestOptions,
            provide(Http, mockHttpProvider),
        ];
    });

    it('http call to obtain data',
        inject(
            [DataService, MockBackend],
            fakeAsync((service: DataService, backend: MockBackend) => {
                backend.connections.subscribe((connection: MockConnection) => {
                    dataFromDbExpected =  'myData';
                    let mockResponseBody: any = 'myData';
                    let response = new ResponseOptions({ body: mockResponseBody });
                    connection.mockRespond(new Response(response));

                });
                const parsedData$ = service.getDataFromDb()
                    .subscribe(response => {
                        console.log(response);
                        expect(response).toEqual(dataFromDbExpected);
                    });
            })));
});

我显然想用 Observable.timer 测试整个函数。我想有人可能想使用 rxjs 框架中的 TestScheduler ,但我怎么能告诉我只重复定时器功能 x 次呢?我在打字稿上下文中找不到任何使用它的文档。

编辑:我正在使用 rxjs 5 beta 6

编辑:为 Angular 2.0.0 最终版本添加了工作示例:

describe('when getData', () => {
    let backend: MockBackend;
    let service: MyService;
    let fakeData: MyData[];
    let response: Response;
    let scheduler: TestScheduler;

    beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
        backend = be;
        service = new MyService(http);
        fakeData = [{myfake: 'data'}];
        let options = new ResponseOptions({ status: 200, body: fakeData });
        response = new Response(options);

        scheduler = new TestScheduler((a, b) => expect(a).toEqual(b));
        const originalTimer = Observable.timer;
        spyOn(Observable, 'timer').and.callFake(function (initialDelay, dueTime) {
            return originalTimer.call(this, initialDelay, dueTime, scheduler);
        });
    }));
    it('Should do myTest', async(inject([], () => {
        backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
        scheduler.schedule(() => {
            service.getMyData().subscribe(
                myData => {
                    expect(myData.length).toBe(3,
                        'should have expected ...');
                });
        }, 2000, null);
        scheduler.flush();
    })));
});

【问题讨论】:

    标签: unit-testing angular rxjs5


    【解决方案1】:

    您需要将 TestScheduler 注入到 beforeEach 部分内的 timer 方法中:

    beforeEach(function() {
      this.scheduler = new TestScheduler();
      this.scheduler.maxFrames = 5000; // Define the max timespan of the scheduler
      const originalTimer = Observable.timer;
      spyOn(Observable, 'timer').and.callFake(function(initialDelay, dueTime) {  
        return originalTimer.call(this, initialDelay, dueTime, this.scheduler);
      });
    });
    

    之后,您可以通过scheduleAbsolute 完全控制时间:

    this.scheduler.schedule(() => {
      // should have been called once
      // You can put your test code here
    }, 1999, null);
    
    this.scheduler.schedule(() => {
      // should have been called twice
      // You can put your test code here
    }, 2000, null);
    
    this.scheduler.schedule(() => {
      // should have been called three times
      // You can put your test code here
    }, 4000, null);
    
    this.scheduler.flush();
    

    您需要scheduler.flush() 来启动TestScheduler。

    编辑:因此,如果您只想测试 X 次,请尽可能频繁地使用调度功能(并使用正确的绝对时间,以毫秒为单位)。

    edit2:我添加了缺少的调度程序启动

    edit3:我改变了它,所以应该使用 RxJs5

    edit4:添加 maxFrames 设置,因为默认值为 750 毫秒,并且会阻止测试运行时间更长的序列。

    【讨论】:

    • 界面好像变了。 TestScheduler 现在期望构造函数中有一个 assertDeepEqual,请参阅link。不过我有点困惑,我应该在创建 TestScheduler 时断言什么?
    • 您显然需要传递您的断言框架(茉莉花)的深度相等断言功能。试试这个:new TestScheduler((a, b) => expect(a).toEqual(b))
    • 好吧,我之前有过这个,所以我越来越接近了。当我现在想安排调度程序时(scheduleAbsolute 被schedule 取代),所以如果我想通过执行scheduler.schedule(null, 2000, () => {dataService.getDataFromDb().subscribe(response => {expect(response).toEqual(getVehiclesFromDBRespose);});}); 来安排任务,似乎该函数永远不会被调用。
    • 哦,我忘了,你有没有scheduler.start()?否则 TestScheduler 处于暂停模式。我会更新答案。
    • 好的,我现在用 RxJs 5 偶然发现了这个。 start 方法由flush() 代替。并且 schedule 方法具有镜像的参数。这是scheduler.schedule(() => console.log('do something'), 2000, null)。您需要记住的是,测试调度程序的最大帧值为 750 毫秒,并且不会超过此值。所以如果你想测试超过 750 毫秒的东西,你需要设置这个值:scheduler.maxFrames = 5000;
    【解决方案2】:

    TestScheduler() 方法有问题,因为 schedule() 箭头函数永远不会执行,所以我找到了另一条路径。

    Observable.timer 函数只返回一个 Observable,所以我从头开始创建了一个来完全控制。

    首先,为观察者创建一个变量:

    let timerObserver: Observer<any>;
    

    现在在beforeEach() 创建间谍并让它返回一个 Observable。在 Observable 中,将您的实例保存到计时器:

    beforeEach(() => {
      spyOn(Observable, 'timer').and.returnValue(Observable.create(
        (observer => {
          timerObserver = observer;
        })
      ));
    });
    

    在测试中,只需触发 Observable:

    it('Some Test',()=>{
      // do stuff if needed
    
      // trigger the fake timer using the Observer reference
      timerObserver.next('');
      timerObserver.complete();
    
      expect(somethingToHappenAfterTimerCompletes).toHaveBeenCalled();
    });
    

    【讨论】:

    • 这对您有什么作用?在我的测试中,我收到一条错误消息“错误::timer() 方法不存在”。
    • 底层库发生了变化,所以这个方案对最新的不适用。 timer() 不再存在于 Observable 上,这可能是您的错误的原因。我们的解决方案是将timer() 函数包装在我们自己的组件和spyOn() 上的自己的方法中。
    • 啊,这就解释了。感谢您的回答!
    【解决方案3】:

    您可以使用 fakeAsync() 轻松测试 Observable 计时器。这是一个显示倒数计时器的组件(使用 momentJS 持续时间):

    timeout.component.ts

    @Component({
      selector: 'app-timeout-modal',
      templateUrl: './timeout-modal.component.html'
    })
    export class TimeoutModalComponent implements OnInit {
      countdownTimer: Observable<number>;
      countdownSubscription: Subscription;
      durationLeft = moment.duration(60000); // millis - 60 seconds
    
      ngOnInit() {
        this.countdownTimer = Observable.timer(0, 1000);
        this.countdownSubscription = this.countdownTimer
          .do(() => this.durationLeft.subtract(1, 's'))
          .takeWhile(seconds => this.durationLeft.asSeconds() >= 0)
          .subscribe(() => {
            if (this.durationLeft.asSeconds() === 0) {
            this.logout();
          }
        });
      }
    }
    

    timeout.component.spec.ts

    beforeEach(async(() => {
        ...
    }));
    
    beforeEach(() => {
        fixture = TestBed.createComponent(TimeoutModalComponent);
        component = fixture.componentInstance;
    });
    
    it('should show a count down', fakeAsync(() => {
        fixture.detectChanges();
        expect(component.durationLeft.asSeconds()).toEqual(60);
        tick(1000);
        fixture.detectChanges();
        expect(component.durationLeft.asSeconds()).toEqual(59);
    
        component.countdownSubscription.unsubscribe();
    }));
    

    【讨论】:

      【解决方案4】:

      我也为此苦苦挣扎了一段时间。由于自从提出这个问题以来,框架显然发生了很多变化,我想也许有人会从我的解决方案中得到帮助。我的项目使用 rxjs 5、jasmine 2.8 和 angular 5。

      在我的组件中,一个计时器用于每分钟调用一次服务中的 http-get 函数。我的问题是,在使用 fakeAsync 区域时,从未调用过(存根)get 函数,并且我收到错误消息:“错误:1 个定期计时器仍在队列中。”。

      出现错误是因为计时器一直在触发并且在测试结束时没有停止。这可以通过添加“discardPeriodicTasks();”来解决到测试结束,这会导致计时器停止。打钩();可以用来假装时间流逝,直到下一次通话。我在我的服务中对我的 get-function 使用了一个间谍来查看它是否有效:

        it(
          'should call getTickets from service every .. ms as defined in refreshTime',
          fakeAsync(() => {
            fixture.detectChanges();
            tick();
            expect(getTicketsSpy).toHaveBeenCalledTimes(1);
            // let 2 * refreshtime pass
            tick(2 * component.refreshTime);
            expect(getTicketsSpy).toHaveBeenCalledTimes(3);
            discardPeriodicTasks();
          })
        );
      

      refreshTime 是我在计时器中使用的参数。我希望这可以防止有人花半天时间来解决这个问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-09-16
        • 2015-11-12
        • 1970-01-01
        • 2015-09-01
        • 2018-07-26
        • 2016-04-24
        • 1970-01-01
        相关资源
        最近更新 更多