简体   繁体   中英

How to test the side effects of a subscription in an Angular component

I have an Angular component, MyComponent , which calls a method returning an Observable and subscribes to it, something like

export class MyComponent {
  someProperty;
  constructor(service: MyService) {}
  someButtonClicked() {
    this.service.doStuffAsync().subscribe(
      resp => this.someProperty = resp;
    );
  }
}

@Injectable()
export MyService {
  doStuffAsync() {
    // returns an Observable which notifies asychronously, e.g. like HttoClient.get(url)
  }
}

I want to test the method someButtonClicked() and therefore I create a MyServiceMock class which I inject in the test

export class MyServiceMock {
  doStuffAsync() {
    return of({// some object}).pipe(observeOn(asyncScheduler));
  }
}

For whatever reason I want doStuffAsync() of MyServiceMock to be asynchronous so observeOn(asyncScheduler) is used.

At this point though I do not know how to test someButtonClicked() . I have tried different strategies, eg the following

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: MyService, useClass: MyServiceMock },
      ]
    }).compileComponents();
  }));

  let fixture: ComponentFixture<MyComponent>;
  let component: MyComponent;

  beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('test someButtonClicked', async(() => {
    component.someButtonClicked();
    fixture.whenStable().then(() => {
      expect(component.someProperty).toBeDefined();
    });
  }));

but this fails since doStuffAsync() of MyServiceMock asynchronous.

So my question is which is the best strategy to test the side effects (ie someProperty being set correctly) induced by a method which subscribes to an asynchronous Observable.

 // All code should be synchronous within this it block
    it('test someButtonClicked', async(() => {
      // sync - button clicks are synchronous
      component.someButtonClicked();

      // but after btn clicks, we need to let some async code to finish
      // like rxjs subscriptions etc.
      await fixture.whenStable();
      // and then continue

      expect(component.someProperty).toBeDefined();
    }));

Don't use then fixture.whenStable().then()

I use a helper function called emitted

export const emitted = obs$ => new Promise(function(resolve, reject) {
  const emitted$ = new Subject();
  obs$.pipe(takeUntil(emitted$)).subscribe(
    _ => {
      emitted$.next();
      emitted$.complete();
      resolve(true);
    },
    _ => {
      emitted$.next();
      emitted$.complete();
      reject('An error occurred');
    }
  )
});

If your mock services shared the same instance of the observable with the component

export class MyServiceMock {
  private obs$ = of({// some object}).pipe(observeOn(asyncScheduler));

  doStuffAsync() {
    return obs$;
  }
}

inside your spec you can await it to have emitted

it('test someButtonClicked', async(async () => {
  component.someButtonClicked();
  const service = TestBed.get(MyService);
  await emitted(service.doStuffAsync());
  expect(component.someProperty).toBeDefined();
}));

Seeing you subscribed to the same instance of the observable as the component you know the component has run it's observer function as well as you subscribed after the component did.

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