Testing redux-observable Epic with debounce operator

1.1k views Asked by At

Having the following redux-observable epic:

export const mouseEventEpic = (action$, store) =>
 action$
  ::filter(action => action.type === MOUSE_OUT || action.type === MOUSE_OVER)
  ::debounceTime(200)
  ::map(action => getMappedAction(action, store));

const getMappedAction = (action, store) => {
 switch (action.type) {
      case MOUSE_OVER:
        return {type: "OVER"};
      case MOUSE_OUT:
        return {type: "OUT"};
 }
};

and the following test

import { expect } from 'chai';
import configureMockStore from 'redux-mock-store';
import { createEpicMiddleware } from 'redux-observable';
import { mouseEventEpic } from '...';
    
const epicMiddleware = createEpicMiddleware(mouseEventEpic );
const mockStore = configureMockStore([epicMiddleware]);

describe('Epic...', () => {
 let store;

 beforeEach(() => {
  store = mockStore({});
 });

 it('test...', () => {    
  store.dispatch({type:'MOUSE_OVER'});    
  expect(store.getActions()).to.deep.equal([]);
 });
});

The store.getActions() returns an array with one action - "MOUSE_OVER". Whereas when removing the debounce it returns another (and the expected) action - "OVER".
I'd like to stub/remove the debounce operator in the test. Tried to follow the ideas in this link using the sinon stub function with no success.
Some guideline on how to mock a RxJS operator or specifically the debounce/throttle would be appreciated.
Using React, Mocha, Chai, Enzyme...

thanks

2

There are 2 answers

0
jayphelps On

There are many ways, one would be to stub it out, but it has to be stubbed out before your epic is started, otherwise the operator has already been called and stubbing it will do nothing (this may be what happened when you tried).

I believe you'd need to move your middleware/store creation logic into the beforeEach, and stub out debounceTime in there too, before you create the middleware/store.

As far as how to stub, one example using sinon is:

// inside `beforeEach()`
const debounceTime = sinon.stub(
  Observable.prototype, 'debounceTime',
  function () {
    // returning the upstream Observable which
    // means debounceTime is now effectively a
    // a pass through (does nothing)
    return this;
  }
);

// example inside a test
assert(debounceTime.withArgs(200).calledOnce);

// later, probably in `afterEach()`
debounceTime.restore();
0
CharlieH On

My second try. Looks like the stub never called probably still called (if at all) sometime later. Again, if removing the ::debounceTime(200) line, everything works just fine.

let sandbox, debounceTime, epicMiddleware, mockStore, store;

beforeEach(() => {
    sandbox = sinon.sandbox.create();
    debounceTime = sandbox.stub(
        Observable.prototype, 'debounceTime',
        () => {             
            console.log('-----------------inside debounce stub') //never called...
            return this;
        }
    );

    epicMiddleware = createEpicMiddleware(combineEpics(stackedOverTimeMouseEventEpic));
    mockStore = configureMockStore([epicMiddleware]);
    store = mockStore({});
});

afterEach(() => {
    epicMiddleware.replaceEpic(stackedOverTimeMouseEventEpic);
    debounceTime.restore();
    sandbox.restore();
});

it('Epic...', () => {
    const ret = store.dispatch({"type": "MOUSE_OVER"});     
    // expect(debounceTime.withArgs(200).calledOnce).to.be.true;    //returns false
    expect(store.getActions()).to.deep.equal([]);                   //returns [{"type": "MOUSE_OVER"},{"type": "@@redux-observable/EPIC_END"}]
});