Search code examples
angularrxjsngrx

Angular NgRx Effect errors in Marble Testing: Expected $.length = 0 to equal 2. / Expected $[0] = undefined to equal Object


I have an app that uses Angular and NgRx and I am having difficulties to test my Effect, using Marble Testing.

The error that I get is:

Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).

Here is the Effect:

@Injectable()
export class OrderLogisticStatusEffects {
  loadOrdersLogisticStatus$ = createEffect(() =>
    this.actions$.pipe(
      ofType(LOAD_ORDERS_LOGISTIC_STATUS),
      withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
      switchMap(([action, pollingInterval]) =>
        timer(0, pollingInterval).pipe(
          withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
          switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
        )
      )
    )
  );

  constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}

  private loadOrdersLogisticStatus(
    pollingInterval: number,
    company: Company
  ): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
    if (!company?.logisticsToken) {
      return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
    }

    this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
      timeout(pollingInterval),
      map((result) => new LoadOrderLogisticStatusSuccess(result)),
      catchError((error) => {
        if (error.name === 'TimeoutError') {
          console.warn('Timeout error while loadin logistic status service', error);
        } else {
          console.error('Error loading order logistic status', error);
          Sentry.captureException(error);
        }

        return of(new LoadOrderLogisticStatusFail(error));
      })
    );
  }
}

And here is my test:

fdescribe('Order Logistic Status Effect', () => {
  let actions$: Observable<Action>;
  let effects: OrderLogisticStatusEffects;

  describe('With a selected company', () => {
    beforeEach(() => {
      const mockState = {
        ordersLogisticStatus: {
          pollingInterval: 10,
        },
        company: {
          selectedCompany: {
            logisticsToken: 'ey.xxxx.yyyy',
          },
        },
      };

      TestBed.configureTestingModule({
        providers: [
          { provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
          OrderLogisticStatusEffects,
          provideMockActions(() => actions$),
          provideMockStore({
            selectors: [
              {
                selector: orderLogisticsStatusPollingIntervalSelector,
                value: 30,
              },
              {
                selector: selectedCompanySelector,
                value: {
                  logisticsToken: 'ey.xxxx.yyy',
                },
              },
            ],
          }),
        ],
      });

      effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
    });

    it('should sucessfully load the orders logistics status', () => {
      const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
      service.getOrdersStatus.and.returnValue(cold('-a|', { a: mockData }));

      actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
      const expected = hot('-a|', {
        a: new LoadOrderLogisticStatusSuccess(mockData),
      });

      getTestScheduler().flush();
      expect(effects.loadOrdersLogisticStatus$).toBeObservable(expected);
    });
  });
});

const mockData = {
  1047522: {
    status: 0,
    partner: {
      id: 1,
    },
    eta: '2020-06-09 10:00',
    pickupEta: '2020-06-09 12:00',
  },
};

The problem seems to be happening with my service mock. Even though I configured it to return a cold observable, it seems that it is returning undefined.

Can anyone help me?

Stack Blitz: https://stackblitz.com/edit/angular-effects-test


Solution

  • StackBlitz.


    There were a few problems:

    Firstly, the loadOrdersLogisticStatus was missing a return:

    loadOrdersLogisticStatus (/* ... */) {
      return this.orderLogisticStatusService.getOrdersStatus(/* ... */)
    }
    

    Then, I found out that jasmine-marbles does not set the AsyncScheduler.delegate automatically, as opposed to TestScheduler.run:

     run<T>(callback: (helpers: RunHelpers) => T): T {
      const prevFrameTimeFactor = TestScheduler.frameTimeFactor;
      const prevMaxFrames = this.maxFrames;
    
      TestScheduler.frameTimeFactor = 1;
      this.maxFrames = Infinity;
      this.runMode = true;
      AsyncScheduler.delegate = this;
    
      /* ... */
    }
    

    This is important because when using marbles, everything is synchronous. But in your implementation, there was a timer(0, pollingInterval) observable, which, by default, uses AsyncScheduler. Without setting AsyncScheduler.delegate, we'd be having asynchronous actions, which I think it was the main problem.

    So, in order to set the delegate property, I've added this line in beforeEach():

    AsyncScheduler.delegate = getTestScheduler();
    

    Finally, I think there was a small problem with your assertions. Your effect property does not seem to ever complete, and you're also using timer(0, pollingInterval). So what I think you could do now is to add the take(N) operator in order to test against N emissions:

    it("should sucessfully load the orders logistics status", () => {
      const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
      service.getOrdersStatus.and.callFake(() => cold('-a|', { a: mockData }));
    
      actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
      const expected = hot('-a--(b|)', {
        a: new LoadOrderLogisticStatusSuccess(mockData),
        b: new LoadOrderLogisticStatusSuccess(mockData),
      });
    
      expect(effects.loadOrdersLogisticStatus$.pipe(take(2))).toBeObservable(expected);
    });
    

    '-a--(b|)' - a is sent at the 10th frame, and b + complete notification(due to take) are sent at 40th frame, because the pollingInterval is 30 and the current frame would be 10 when the second notification(b) would be scheduled.