3

I'm trying to write a test function to a redux-observable epic. The epic works fine inside the app and I can check that it correctly debounces the actions and waits 300ms before emitting. But for some reason while I'm trying to test it with jest the debounce operator triggers immediately. So my test case to ensure that the debounce is working fails.

This is the test case

it('shall not invoke the movies service neither dispatch stuff if the we invoke before 300ms', done => {
    const $action = ActionsObservable.of(moviesActions.loadMovies('rambo'));

    loadMoviesEpic($action).subscribe(actual => {
        throw new Error('Should not have been invoked');
    });

    setTimeout(() => {
        expect(spy).toHaveBeenCalledTimes(0);
        done();
    }, 200);

});

this is my spy definition.

jest.mock('services/moviesService');

const spy = jest.spyOn(moviesService, 'searchMovies');

beforeEach(() => {
    moviesService.searchMovies.mockImplementation(keyword => {
        return Promise.resolve(moviesResult);
    });
    spy.mockClear();
});

and this is the epic

import { Observable, interval } from 'rxjs';
import { combineEpics } from 'redux-observable';

import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounce';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/interval';
import 'rxjs/add/observable/concat';

import actionTypes from 'actions/actionTypes';
import * as moviesService from 'services/moviesService';
import * as moviesActions from 'actions/moviesActions';

const DEBOUNCE_INTERVAL_IN_MS = 300;
const MIN_MOVIES_SEARCH_LENGTH = 3;

export function loadMoviesEpic($action) {
  return $action
    .ofType(actionTypes.MOVIES.LOAD_MOVIES)
    .debounce(() => Observable.interval(DEBOUNCE_INTERVAL_IN_MS))
    .filter(({ payload }) => payload.length >= MIN_MOVIES_SEARCH_LENGTH)
    .switchMap(({ payload }) => {
      const loadingAction = Observable.of(moviesActions.loadingMovies());

      const moviesResultAction = Observable.from(
        moviesService.searchMovies(payload)
      )
        .map(moviesResultList => moviesActions.moviesLoaded(moviesResultList))
        .catch(err => Observable.of(moviesActions.loadError(err)));

      return Observable.concat(loadingAction, moviesResultAction);
    });
}

const rootEpic = combineEpics(loadMoviesEpic);

export default rootEpic;

so basically this thing shall not be called, because the debounce time is 300ms and I'm trying to check the spy after 200ms. But after 10ms the spy is being invoked.

How can I properly test this epic? I accept any suggestion but preferably I would like to avoid marble testing and rely only on timers and fake timers.

Thanks :D

skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • I had exactly the same problem as yours several months ago and I finally ended up using marble diagram ^^ https://stackoverflow.com/q/52832980/5692151 – Lin Shen Mar 24 '19 at 19:22

2 Answers2

3

The issue is that debounce and debounceTime emit right away when they reach the end of the observable they are debouncing.

So since you are emitting with of, the end of the observable is reached and debounce allows the last emitted value through right away.

Here is a simple test that demonstrates the behavior:

import { of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

test('of', () => {
  const spy = jest.fn();
  of(1, 2, 3)
    .pipe(debounceTime(1000000))
    .subscribe(v => spy(v));
  expect(spy).toHaveBeenCalledWith(3);  // 3 is emitted immediately
})

To effectively test debounce or debounceTime you need to use an observable that keeps emitting and doesn't end using something like interval:

import { interval } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

test('interval', done => {
  const spy = jest.fn();
  interval(100)
    .pipe(debounceTime(500))
    .subscribe(v => spy(v));
  setTimeout(() => {
    expect(spy).not.toHaveBeenCalled();  // Success!
    done();
  }, 1000);
});
Brian Adams
  • 43,011
  • 9
  • 113
  • 111
1

Instead of setTimeout try advanceTimersByTime

Cold Fridge
  • 113
  • 8
  • the main problem here is that the debounce function triggers immediately instead of waiting 300ms, so advanceTimersByTime dont't really applies here as I'm not trying to advance the time. But trying to understand why the debounce triggers immediately during my tests. – Victor Ribeiro da Silva Eloy Mar 19 '19 at 13:48
  • Oh I see, in the question it seemed like you said the setTimeout was invoked after 10ms. – Cold Fridge Mar 19 '19 at 13:56