Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test Observable.ajax (redux-observable)?

I have been playing with rxjs and redux-observable for the last few days and have been struggle to find a way to a test for Observable.ajax. I have the following epic which create a request to https://jsonplaceholder.typicode.com/,

export function testApiEpic (action$) {
  return action$.ofType(REQUEST)
    .switchMap(action =>
      Observable.ajax({ url, method })
        .map(data => successTestApi(data.response))
        .catch(error => failureTestApi(error))
        .takeUntil(action$.ofType(CLEAR))
    )
}

where,

export const REQUEST = 'my-app/testApi/REQUEST'
export const SUCCESS = 'my-app/testApi/SUCCESS'
export const FAILURE = 'my-app/testApi/FAILURE'
export const CLEAR = 'my-app/testApi/CLEAR'

export function requestTestApi () {
  return { type: REQUEST }
}
export function successTestApi (response) {
  return { type: SUCCESS, response }
}
export function failureTestApi (error) {
  return { type: FAILURE, error }
}
export function clearTestApi () {
  return { type: CLEAR }
}

The code works fine when runs in browser but not when testing with Jest.

I have try,

1) Create a test based on https://redux-observable.js.org/docs/recipes/WritingTests.html. The store.getActions() returns only { type: REQUEST }.

const epicMiddleware = createEpicMiddleware(testApiEpic)
const mockStore = configureMockStore([epicMiddleware])

describe.only('fetchUserEpic', () => {
  let store

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

  afterEach(() => {
    epicMiddleware.replaceEpic(testApiEpic)
  })

  it('returns a response, () => {
    store.dispatch({ type: REQUEST })
    expect(store.getActions()).toEqual([
      { type: REQUEST },
      { type: SUCCESS, response }
    ])
  })
})

2) Create a test based on Redux-observable: failed jest test for epic. It returns with

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

  it('returns a response', (done) => {
    const action$ = ActionsObservable.of({ type: REQUEST })
    const store = { getState: () => {} }
    testApiEpic(action$, store)
      .toArray()
      .subscribe(actions => {
        expect(actions).to.deep.equal([
          { type: SUCCESS, response }
        ])
        done()
      })
  })

Can someone point me out what is the correct way to test Observable.ajax ?

like image 661
binkpitch Avatar asked Mar 08 '23 23:03

binkpitch


1 Answers

I would follow the second example, from StackOverflow. To make it work you'll need to make some minor adjustments. Instead of importing Observable.ajax in your epic file and using that reference directly, you need to use some form of dependency injection. One way is to provide it to the middleware when you create it.

import { ajax } from 'rxjs/observable/dom/ajax';

const epicMiddleware = createEpicMiddleware(rootEpic, {
  dependencies: { ajax }
});

The object we passed as dependencies will be give to all epics as the third argument

export function testApiEpic (action$, store, { ajax }) {
  return action$.ofType(REQUEST)
    .switchMap(action =>
      ajax({ url, method })
        .map(data => successTestApi(data.response))
        .catch(error => failureTestApi(error))
        .takeUntil(action$.ofType(CLEAR))
    );
}

Alternatively, you could not use the dependencies option of the middleware and instead just use default parameters:

export function testApiEpic (action$, store, ajax = Observable.ajax) {
  return action$.ofType(REQUEST)
    .switchMap(action =>
      ajax({ url, method })
        .map(data => successTestApi(data.response))
        .catch(error => failureTestApi(error))
        .takeUntil(action$.ofType(CLEAR))
    );
}

Either one you choose, when we test the epic we can now call it directly and provide our own mock for it. Here are examples for success/error/cancel paths These are untested and might have issues, but should give you the general idea

it('handles success path', (done) => {
  const action$ = ActionsObservable.of(requestTestApi())
  const store = null; // not used by epic
  const dependencies = {
    ajax: (url, method) => Observable.of({ url, method })
  };

  testApiEpic(action$, store, dependencies)
    .toArray()
    .subscribe(actions => {
      expect(actions).to.deep.equal([
        successTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
      ])

      done();
    });
});

it('handles error path', (done) => {
  const action$ = ActionsObservable.of(requestTestApi())
  const store = null; // not used by epic
  const dependencies = {
    ajax: (url, method) => Observable.throw({ url, method })
  };

  testApiEpic(action$, store, dependencies)
    .toArray()
    .subscribe(actions => {
      expect(actions).to.deep.equal([
        failureTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
      ])

      done();
    });
});

it('supports cancellation', (done) => {
  const action$ = ActionsObservable.of(requestTestApi(), clearTestApi())
  const store = null; // not used by epic
  const dependencies = {
    ajax: (url, method) => Observable.of({ url, method }).delay(100)
  };
  const onNext = chai.spy();

  testApiEpic(action$, store, dependencies)
    .toArray()
    .subscribe({
      next: onNext,
      complete: () => {
        onNext.should.not.have.been.called();        
        done();
      }
    });
});
like image 115
jayphelps Avatar answered Mar 21 '23 00:03

jayphelps