简体   繁体   中英

Unit Test RxJS Observable.timer using typescript, karma and jasmine

Hi I'm relatively new to Angular2, Karma and Jasmine. Currently I'm using Angular 2 RC4 Jasmine 2.4.x I have an Angular 2 service which periodically calls an http service like this:

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

Now I want to test the functionality. For testing purposes I have just tested the "http.get" on a separate function without the Observable.timer by doing:

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);
                    });
            })));
});

I obviously want to test the whole function with the Observable.timer. I think one might want to use the TestScheduler from the rxjs framework, but how can I tell to only repeat the timer function for x times? I couln't find any documentation using it in the typescript context.

Edit: I'm using rxjs 5 beta 6

Edit: Added working example for Angular 2.0.0 final release:

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();
    })));
});

You need to inject the TestScheduler into the timer method inside a beforeEach part:

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);
  });
});

After that you have full control of the time with 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();

You need scheduler.flush() to start the TestScheduler.

edit: so if you want to only test it X times, use the schedule functions as often (and with the right absolute times in milliseconds) as you wish.

edit2: I added the missing scheduler start

edit3: I changed it so should be working with RxJs5

edit4: Add maxFrames setting since the default is 750ms and will prevent testing longer-running sequences.

I had issues with the TestScheduler() approach because the schedule() arrow function would never execute, so I found another path.

The Observable.timer function just returns an Observable, so I created one from scratch to give me complete control.

First, create a var for the observer:

let timerObserver: Observer<any>;

Now in the beforeEach() create the spy and have it return an Observable. Inside the Observable, save your instance to the timer:

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

In the test, just trigger the Observable:

it('Some Test',()=>{
  // do stuff if needed

  // trigger the fake timer using the Observer reference
  timerObserver.next('');
  timerObserver.complete();

  expect(somethingToHappenAfterTimerCompletes).toHaveBeenCalled();
});

You can test Observable timers pretty easily with fakeAsync() . Here's a component that displays a countdown timer (using a momentJS duration):

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();
}));

I was struggling with this for a while also. Since apparently a lot has changed in the frameworks since this question was asked, I thought maybe someone would be helped by my solution. My project uses rxjs 5, jasmine 2.8 and angular 5.

In my component a timer was used to call a http-get function in a service every minute. My problem was that when using fakeAsync zone the (stubbed) get function was never called and I received the error: "Error: 1 periodic timer(s) still in the queue.".

The error is showing up because the timer keeps firing and isn't stopped at the end of the test. This can be resolved by adding "discardPeriodicTasks();"to the end of the test, which causes the timer to stop. Tick(); can be used to fake to passage of time untill a next call. I used a spy on my get-function in my service to see if it worked:

  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();
    })
  );

The refreshTime is the parameter that I used in the timer. I hope this prevents someone from spending half a day trying to figure this out.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM