问题
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();
})));
});
回答1:
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.
回答2:
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();
});
回答3:
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.
回答4:
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();
}));
来源:https://stackoverflow.com/questions/39167415/unit-test-rxjs-observable-timer-using-typescript-karma-and-jasmine