Testing NGRX effect with delay
Asked Answered
B

4

8

I want to test an effect that works as follows:

  1. Effect starts if LoadEntriesSucces action was dispatched
  2. It waits for 5 seconds
  3. After 5 seconds passes http request is send
  4. When response arrives, new action is dispatched (depending, whether response was succes or error).

Effect's code looks like this:

  @Effect()
  continuePollingEntries$ = this.actions$.pipe(
    ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
    delay(5000),
    switchMap(() => {
      return this.subnetBrowserService.getSubnetEntries().pipe(
        map((entries) => {
          return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
        }),
        catchError((error) => {
          return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
        }),
      );
    }),
  );

What I want to test is whether an effect is dispatched after 5 seconds:

it('should dispatch action after 5 seconds', () => {
  const entries: SubnetEntry[] = [{
    type: 'type',
    userText: 'userText',
    ipAddress: '0.0.0.0'
  }];

  const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
  const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});

  actions$ = hot('-a', { a: action });
  const response = cold('-a', {a: entries});
  const expected = cold('- 5s b ', { b: completion });

  subnetBrowserService.getSubnetEntries = () => (response);

  expect(effects.continuePollingEntries$).toBeObservable(expected);
});

However this test does not work for me. Output from test looks like this:

Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).

What should I do to make this test work?

Bawl answered 29/1, 2019 at 15:0 Comment(3)
Did you ever found a fix? I ready about using TestScheduler, but never got it to work. dev.to/mokkapps/…Chamfron
Nope - I ended up not testing it at all :< Seems to me like jasmine-marbles is not maintained anymore, and there is some stuff that just don't work (like time schedulers)Bawl
See my response below. It worked for me. You can also add a parameter to your effect to disable the timer, so you do not have to worry about passing the seconds delay. Let me know if you need help.Chamfron
C
6

Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.

We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.

For example, let's unit test the following effect:

effectWithDelay$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(fromFooActions.doSomething),
    delay(5000),
    switchMap(({ payload }) => {
      const { someData } = payload;

      return this.fooService.someMethod(someData).pipe(
        map(() => {
          return fromFooActions.doSomethingSuccess();
        }),
        catchError(() => {
          return of(fromFooActions.doSomethinfError());
        }),
      );
    }),
  );
});

The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:

import { TestBed } from "@angular/core/testing";

import { provideMockActions } from "@ngrx/effects/testing";

import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";

import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";

// ...

describe("FooEffects", () => {
  let actions$: Observable<unknown>;

  let testScheduler: TestScheduler; // <-- instance of the test scheduler

  let effects: FooEffects;
  let fooServiceMock: jasmine.SpyObj<FooService>;

  beforeEach(() => {
    // Initialize the TestScheduler instance passing a function to
    // compare if two objects are equal
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });

    TestBed.configureTestingModule({
      imports: [],
      providers: [
        FooEffects,
        provideMockActions(() => actions$),

        // Mock the service so that we can test if it was called
        // and if the right data was sent
        {
          provide: FooService,
          useValue: jasmine.createSpyObj("FooService", {
            someMethod: jasmine.createSpy(),
          }),
        },
      ],
    });

    effects = TestBed.inject(FooEffects);
    fooServiceMock = TestBed.inject(FooService);
  });

  describe("effectWithDelay$", () => {
    it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
      const someDataMock = { someData: Math.random() * 100 };

      const initialAction = fromFooActions.doSomething(someDataMock);
      const expectedAction = fromFooActions.doSomethingSuccess();
    
      testScheduler.run((helpers) => {

        // When the code inside this callback is being executed, any operator 
        // that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
        // **automatically** use the TestScheduler instead, so that we have 
        // "virtual time". You do not need to pass the TestScheduler to them, 
        // like in the past.
        // https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing

        const { hot, cold, expectObservable } = helpers;

        // Actions // -a-
        // Service //    -b|
        // Results // 5s --c

        // Actions
        actions$ = hot("-a-", { a: initialAction });

        // Service
        fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));

        // Results
        expectObservable(effects.effectWithDelay$).toBe("5s --c", {
          c: expectedAction,
        });
      });

      // This needs to be outside of the run() callback
      // since it's executed synchronously :O
      expect(fooServiceMock.someMethod).toHaveBeenCalled();
      expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
      expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
    });
  });
});

Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.

Cementite answered 23/1, 2021 at 10:2 Comment(3)
And this is what we've ended up using too.Bawl
I've also implemented this approach for my async effects. Very helpful!Kerk
Thanks for this. It is such a detailed answer.Defibrillator
A
1

you could use the done callback from jasmine

it('should dispatch action after 5 seconds', (done) => {
  const resMock = 'resMock';
  const entries: SubnetEntry[] = [{
    type: 'type',
    userText: 'userText',
    ipAddress: '0.0.0.0'
  }];

  const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
  const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});

  actions$ = hot('-a', { a: action });
  const response = cold('-a', {a: entries});
  const expected = cold('- 5s b ', { b: completion });

  subnetBrowserService.getSubnetEntries = () => (response);
  effects.continuePollingEntries$.subscribe((res)=>{
    expect(res).toEqual(resMock);
    done()
  })
});
Ambie answered 29/1, 2019 at 15:7 Comment(4)
It does not test whether response came after 5 seconds at all :(Bawl
you want to test to test you'r effect delay time?Ambie
Well, I guess what I want to test whether whole observable stream is same as I expect it to be. In your example it is never tested whether this stream looks as I expect it to be (see 'expected' variable)Bawl
of course, what I meant in this example is that you will provide a mock object with your response :)Ambie
A
1

The second notation doesn't work with jasmine-marbles, use dashes instead:

 const expected = cold('------b ', { b: completion });
Armand answered 29/1, 2019 at 18:56 Comment(1)
the problem with marbels as is is that the result will come empty as the effect is delayed by the delay operator. You will need to change the scheduler and switch to delayWhen. To me, seems like the delay even if its delay(0), it still ticks and this makes a delay and out of sync between you r unit test expected result and your effect, but with delayWhen, this does not happen.Chamfron
C
1

You will need to do 3 things

1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:

    import { async } from 'rxjs/internal/scheduler/async';
    import { cold, hot, getTestScheduler } from 'jasmine-marbles';
    beforeEach(() => {.....
        const testScheduler = getTestScheduler();
        async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})

2- Replace delay, with delayWhen as follows: delayWhen(_x => (true ? interval(50) : of(undefined)))

3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:

const expected = cold('------b ', { b: outcome });
Chamfron answered 2/2, 2019 at 3:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.