0
votes

I'm using NGRX, and use Effects to send HTTP requests. If a user sends another request, any previous request should be cancelled. It is working fine when I test manually, but I want to be able to unit test this. To test this, I'm mocking the service that sends the HTTP request and send a response after a certain delay. Then, I've got a Marble hot observable that triggers 4 requests. I expect that my Effect is only triggered once. However, it is not triggered at all.

The unit test:

it('should only do one request at a time', fakeAsync(() => {
    // Arrange
    const data = createTestData();
    const dataServiceSpy = TestBed.get(DataService);
    dataServiceSpy.getData = jest.fn(
        (query: DataQuery) => {
            const waitTime = 1000 * + query.index;
            return of(assets).pipe(delay(waitTime));
        }
    );         
    // Act
    actions = hot('-abcd-|', {
        a: new SearchData({ index: '6' }),
        b: new SearchData({ index: '5' }),
        c: new SearchData({ index: '4' }),
        d: new SearchData({ index: '1' })
    });

    tick(10000);

    // Assert
    expect(effects.loadData$).toBeObservable(
        hot('-a-|', { a: new SearchDataComplete(assets) })
    );
}));

So, I'm sending 4 search requests; the first one should return data after 6 seconds, second one after 5, and so on. However, my unit test is failing that loadData$ is an empty observable while it expects to have one item.

If I remove the delay in the spy, it works as expected and loadData$ contains 4 results.

My Effect is using NX DataPersistence which takes care of the cancellation if you supply the id function; it will cancel the initial request if a new action comes in with the same id. It is similar to use this.actions$.pipe(switchMap(...))

@Effect()
loadData$ = this.dataPersistence.fetch(ActionTypes.SearchData, {
    id:  (action, state) => {
        return action.type
    },

    run: (action, state) => {
        return this.dataService
            .searchData(action.payload)
            .pipe(                   
                map(data => new SearchDataComplete(data))
            );
    },
1
Not sure if that's a typo but you mocked getData and your effect has searchData - electrichead
Thank you for your reply! I've anonymized the function names, made a typo indeed. The actual function names in my tests are correct. - Boland

1 Answers

2
votes

So I dug a little into this. I have two thoughts:

  1. In a unit test we really just want to test the code that we write. If I am using a third-party lib, I would make an assumption that it is properly unit tested (say, by looking at the source code for that lib). The unit tests for DataPersistence don't test for the cancelation right now (because we are using switchMap and making the assumption that its functionality works).
  2. There is an actual issue when trying to test with delay in the example

In your tests, the tick fires before the Effect is subscribed to (when you call the expect below that).

One way to get around that is as below:

describe('loadTest$', () => {
    it('should only do one request at a time', () => {

      // create a synchronous scheduler (VirtualTime)
      const scheduler = new VirtualTimeScheduler();

      // Arrange
      const data = createTestData();
      const dataServiceSpy = TestBed.get(DataService);

      TestBed.configureTestingModule({
        imports: [NxModule.forRoot(), StoreModule.forRoot({})],
        providers: [
          TestEffects,
          provideMockActions(() => actions),
          {
            provide: DataService,
            useValue: {
              searchData: jest.fn(
                  (query: DataQuery) => {
                      const waitTime = 1000 * + query.index;
                      return of(assets).pipe(delay(waitTime, scheduler));
                  }
              )
            }
          }
        ]
      });

      actions = of(
          new SearchData({ index: '6' }),
          new SearchData({ index: '5' }),
          new SearchData({ index: '4' }),
          new SearchData({ index: '1' })
      );

      const res = [];
      effects.loadData$.subscribe(val => res.push(val));

      scheduler.flush(); // we flush the observable here

      expect(res).toEqual([{ a: new SearchDataComplete(assets) }]);
    });
});

We can use a synchronous scheduler and manually flush it. Normally we wouldn't need to do any of this; it's just because of delay that we need to (it would also happen with other operators that need the scheduler like debounceTime).

Hope this helps. I think your tests would not need to test the functionality of the underlying library (it might not be a strict unit test at that point but more of an integration test).