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

4

8

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();
    })));
});
Carbazole answered 26/8, 2016 at 13:23 Comment(0)
C
9

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.

Cahn answered 28/8, 2016 at 10:11 Comment(9)
It seems the the Interface has changed. TestScheduler now expects a assertDeepEqual in the constructor, see link. I'm a bit confused though, what should I assert on creation of the TestScheduler?Carbazole
You apparently need to pass the deep equalty assertion function of your assertion framework (jasmine). Try it with this: new TestScheduler((a, b) => expect(a).toEqual(b))Cahn
Alright, I had this earlier, so I'm getting closer. When I now want to schedule the scheduler (scheduleAbsolute was replaced by schedule), so if I want to schedule the task by doing scheduler.schedule(null, 2000, () => {dataService.getDataFromDb().subscribe(response => {expect(response).toEqual(getVehiclesFromDBRespose);});}); it seems the function is never called.Carbazole
Oh, I forgot, have you done scheduler.start()? Otherwise the TestScheduler is in pause mode. I'll update the answer.Cahn
The start() function also does not exist any more. It seems the TestScheduler in version 5 has changed fundamentally compared to version 4. Others had similar problems in the github repo. The official documentation has no further detail either. I'd be very happy if some could enlighten me.Carbazole
Ok, I stumbled on this myself with RxJs 5 now. The start method is replaced by flush(). And the schedule method has the parameters mirrored. It's scheduler.schedule(() => console.log('do something'), 2000, null). The thing you need to keep in mind that the test scheduler has a maximum frame value of 750ms and doesn't go past this. So if you want to test something past 750ms you need to set this value: scheduler.maxFrames = 5000;.Cahn
could you look at this question too? i'm having some troubles... #43674651Crissman
This approach was not working for me. The way the TestScheduler() was initialized was flagged as an error by IntelliJ and the code in the schedule() blocks would never run, even after running the flush command. I ended up creating an Observerable from scratch and using next() to resolve it.Chiarra
How about moving the timer ahead using jasmine.clock().tick(durationInMs); ? and asserting if the api call is made as many number of times as expected ?Paulettapaulette
C
1

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();
});
Chiarra answered 4/4, 2018 at 21:7 Comment(3)
How is this working for you? In my tests I get an error saying "Error: <spyOn> : timer() method does not exist".Temper
There was a change in the underlying library, so this solution is not applicable to the latest. timer() no longer exists on Observable, which is probably the cause of your error. Our solution was to wrap the timer() function in it's own method on our own component and spyOn() that.Chiarra
Ah that explains it. Thanks for your answer!Temper
H
1

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();
}));
Hectic answered 29/8, 2018 at 18:34 Comment(0)
T
0

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.

Temper answered 20/7, 2018 at 13:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.