Testing NGRX effect that emits action with delay OR does not emit anything

1.2k Views Asked by At

I have an NGRX effect that - depending on the state - emits an action with a delay or it emits nothing. I want to write a test, covering both situations.

This is the effect:

myEffect$ = createEffect(() =>
  this.actions$.pipe(
    ofType(MyAction),
    filter(state => state.foo === false),
    delay(4000),
    map(state => myOtherAction())
  )
);

The test for the situation where it should emit the otherAction with the delay works fine:

describe('emit my action', () => {
   const action = MyAction();

   it('should return a stream with myOtherAction', () => {
      const scheduler = getTestScheduler();
      scheduler.run(helpers => {
        // build the observable with the action
        actions = hot('-a', { a: action });

        // define what is the expected outcome of the effect
        const expected = {
           b: MyOtherAction()
        };
        helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
      });
   });
});

But I have no clue how to test the other state, where it should NOT emit another action (the stream has zero length):

   it('should return an empty stream', () => {
      store.setState({
        myFeature: {
           foo: true
        }
      });
      // ???
   });

Please help :)

2

There are 2 best solutions below

0
On BEST ANSWER

Due to the hint from AliF50, I replaced the filter in the chain (which stops the Observable from emitting), by a "Noop Action" (= a normal action without any listeners on it). So instead of checking the foo property in the filter, I return the noopAction in the map, when foo is true, and the otherAction when it's false.

The effect:

myEffect$ = createEffect(() =>
  this.actions$.pipe(
    ofType(MyAction),
    //filter(state => state.foo === false),
    delay(4000),
    map(state => state.foo !== false ? noopAction() : myOtherAction())
  )
);

The test:

describe('emit my action', () => {
  const action = MyAction();

  it('should return a stream with myOtherAction', () => {
    const scheduler = getTestScheduler();
    scheduler.run(helpers => {
      // build the observable with the action
      actions = hot('-a', { a: action });
      // define what is the expected outcome of the effect
      const expected = {
        b: MyOtherAction()
      };
      helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
    });
  });

  it('should return a stream with noop action as foo is true', () => {
    store.setState({
      myFeature: {
        foo: true
      }
    });
    const scheduler = getTestScheduler();
    scheduler.run(helpers => {
      // build the observable with the action
      actions = hot('-a', { a: action });
      // define what is the expected outcome of the effect
      const expected = {
        b: NoopAction()
      };
      helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
    });
  });

});
1
On

This will be tough to do because the filter will prevent the effect to ever return an observable.

Option 1: // wherever you're dispatching MyAction, only dispatch it if the foo property is true on the created action

Option 2: // Change the structure of the effect to return empty

import { EMPTY } from 'rxjs';
....
myEffect$ = createEffect(() =>
  this.actions$.pipe(
    ofType(MyAction),
    delay(4000),
    map(state => state.foo ? myOtherAction() : EMPTY)
  )
);

The test:

import { EMPTY } from 'rxjs';
....
describe('emit my action', () => {
   const action = MyAction();
   action.foo = false; // set foo property to false

   it('should return a stream with myOtherAction', () => {
      const scheduler = getTestScheduler();
      scheduler.run(helpers => {
        // build the observable with the action
        actions = hot('-a', { a: action });

        // define what is the expected outcome of the effect
        const expected = {
           b: EMPTY // assert now it is empty
        };
        helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
      });
   });
})