Michal Dymel
Michal Dymel

Reputation: 4360

Jasmine: testing observables with events

I am trying to test an angular2 application. I have a login form, which uses an observable to send data to the backend:

doLogin() {
    this.usersService.login(this.model)
        .subscribe((data) => {
            console.log("In observable: " + data.isSuccess);
            if (!data.isSuccess) {
                this.alerts.push({});
            }
        });
}

In tests I am adding a spy on the service function, which returns observable, so that component can work on it:

 usersService.login.and.returnValue(Observable.of(
    <LoginResponse>{
        isSuccess: true
    }));

When everything is ready, I dispatch an event on submit button, which triggers doLogin function in component:

submitButton.dispatchEvent(new Event("click"));
fixture.detectChanges();

It works correctly. Unfortunately, when I check if usersService.login has been called in the test:

expect(usersService.login).toHaveBeenCalled();

I get an error, because the observable didn't finish and login has not been called yet.

How should I make sure, I check my spy after observable has finished?

Upvotes: 2

Views: 6997

Answers (1)

Thierry Templier
Thierry Templier

Reputation: 202156

I don't know how you configure the service on the component but it works for me when I override providers of the component created from TestComponentBuilder.

Let's take a sample. I have a service that returns a list of string:

import {Observable} from 'rxjs/Rx';

export class MyService {
  getDogs() {
    return Observable.of([ 's1', 's2', ... ]);
  }
}

A component uses this service to display a list asynchronously when clicking a button:

@Component({
  selector: 'my-list',
  providers: [MyService],
  template: `
    <ul><li *ngFor="#item of items">{{ item }}</li></ul>
    <div id="test" (click)="test()">Test</div>
  `
})
export class MyList implements OnInit {
  items:Array<string>;
  service:MyService;

  constructor(private service:MyService) {
  }

  test() {
    this.service.getDogs().subscribe(
      (dogs) => {
        this.items = dogs;
      });
  }
}

I want to test that when I click on the "Test" button, the test method of the component is called and the getDogs method of the service is indirectly called.

For this, I create a test that instantiate directly the service and load the component using TestComponentBuilder. In this case, I need to call the overrideProviders method on it before calling createAsync. This way, you will be able to provide your spied service to be notified of the call. Here is a sample:

let service:MyService = new MyService();

beforeEach(() => {
    spyOn(service, 'getDogs').and.returnValue(Observable.of(
        ['dog1', 'dog2', 'dog3']));

});

it('should test get dogs', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb.overrideProviders(MyList, [provide(MyService, { useValue: service })])
              .createAsync(MyList).then((componentFixture: ComponentFixture) => {
        const element = componentFixture.nativeElement;
        componentFixture.detectChanges();

        var clickButton = document.getElementById('test');
        clickButton.dispatchEvent(new Event("click"));

        expect(service.getDogs).toHaveBeenCalled();
    });
}));

Edit

Since the event is triggered asynchronously, you could consider to use fakeAsync. The latter allows you to completly control when asynchronous processing are handled and turn asynchronous things in to synchronous ones.

You could wrap your test processing into

fakeAsync((): void => {
  var clickButton = document.getElementById('test');
  clickButton.dispatchEvent(new Event("click"));

  expect(service.getDogs).toHaveBeenCalled();
});

For more details, you could have a look at this question:

Upvotes: 2

Related Questions