Search code examples
angularrxjsjasmineswitchmap

How to execute switchMap and tap rxJS functions from Jasmine test?


I have the following test

let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let loader: HarnessLoader;
  const items: MyItem[] = [
    {
      id: '66249535-31bf-41f3-8f55-8a14877c6d7e',
      status: 'New'
    }
  ];

  const myServiceSpy = jasmine.createSpyObj<MyService>(
    'MyService',
    {
      getItems: of(items),
      changeItemStatus: of()
    }
  );

  const alertsServiceSpy = jasmine.createSpyObj<AlertsService>('AlertsService', [
    'error',
    'warning',
    'success',
    'info'
  ]);

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [MyComponent, TableComponent],
      providers: [{ provide: MyService, useValue: myServiceSpy }, { provide: AlertsService, useValue: alertsServiceSpy}],
      imports: [MatTableModule, SharedModule]
    }).compileComponents();
  }));

  beforeEach(() => {
    myServiceSpy.getItems.calls.reset();
    fixture = TestBed.createComponent(MyComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
    component = fixture.componentInstance;
    component.myId = 123;
    fixture.detectChanges();
  });

  afterAll(() => {
    myServiceSpy.getItems.and.returnValue(of(items));
  });

  it('should call to change the item status if the slider is toggled', async(() => {
    component.onItemChange(items[0], { checked: true, event: { source: { checked: true }}});
    component.onItemChange(items[0], { checked: false, event: { source: { checked: false }}});
  
    fixture.whenStable().then(() => {
      fixture.detectChanges();
      expect(myServiceSpy.changeItemStatus).toHaveBeenCalled();
    });
  }));

And the following component

ngOnInit() {
    const updatedItems$ = this.itemChangeSubject.asObservable().pipe(
      switchMap(itemChange => {
        this.itemBeingChanged = itemChange.item;
        this.toggleEvent = itemChange.event;
        return this.myService.changeItemStatus(
          this.myId,
          itemChange.item.id
        )
        .pipe(catchError(() => {
          this.clicked = false;
          this.toggleEvent.source.checked = ! this.toggleEvent.source.checked;
          return empty();
        }));
      }), 
      tap({
        next: () => {
          const itemStatus = this.itemBeingChanged.isItemChanged ? 'active' : new';
          this.clicked = false;
          this.alerts.success({
            message: `Item is now ${itemStatus}.`
          });
        }
      }),
      switchMap(() => this.myService.getItems(
        this.myId
      )
      .pipe(catchError(() => {
        return empty();
      }))
      )
    );

    this.items$ = this.myService
      .getItems(this.myId)
      .pipe(catchError(() => {
        return empty();
      }))
      .pipe(concat(updatedItems$))
  }

  onItemChange(item: MyItem, event) {
    this.itemChangeSubject.next({ item, event });
  }

My test executes the first switchMap within the ngOnInit as expected, however neither the tap side effect or the following switchMap is executed. How can I modify my test so I can test this entire chain, rather than just the first switchMap? Additionally, is it necessary to even test the tap or error blocks? I'm mostly concerned about the final switchMap since it does make a call to a service.


Solution

  • I think it's due to changeItemStatus: of().

    of is defined as follows:

    return fromArray(args as T[]);
    

    which will eventually reach subscribeToArray:

    export const subscribeToArray = <T>(array: ArrayLike<T>) => (subscriber: Subscriber<T>) => {
      for (let i = 0, len = array.length; i < len && !subscriber.closed; i++) {
        subscriber.next(array[i]);
      }
      subscriber.complete();
    };
    

    So, when using of() the array from above will be empty, which means that the Observable won't emit anything. As a result, the next operators in the chain won't be reached.

    It should work if you replace of() with of(null).