First:
I insert custom middleware into Redux. All this does is look for a Callback for a specific Action.Type.
var callbacksAfterActionType = {};
function reduxTestMiddleware() {
return (next) =>
(action) => {
let retval;
try {
retval = next(action);
if (callbacksAfterActionType[action.type]) {
let cb = callbacksAfterActionType[action.type];
delete callbacksAfterActionType[action.type];
cb();
}
} catch (ex) {
console.error(action, ex);
}
return retval;
}
}
When I create the Redux Store, I insert the Epic middle ware, and the Test Middleware.
let reduxStore =
createStore(rootReducer, applyMiddleware(createEpicMiddleware(rootEpic),reduxTestMiddleware));
Then I use this helper function to dispatch [Action]'s and wait for a specific Action.
function dispatchAndReceiveActions(sendAction, resolveActionType){
return new Promise((resolve, reject) => {
callbacksAfterActionType[resolveActionType] = resolve;
if(Array.isArray(sendAction)) {
sendAction.forEach((action) => {
store().dispatch(action)
});
} else {
store().dispatch(sendAction);
}
});
}
In my Tests, this is how I use it. It sends two actions, SELECT_DEVICE, and DISCONNECT at the same time, and waits for a DISCONNECT_COMPLETE action to be sent to the redux store. You can dispatch One-or-More actions, and wait for ONE action.
it('device connection can be cancelled before being fully established', () => {
return dispatchAndReceiveActions([
{type: "SELECT_DEVICE", device: device},
{type: "DISCONNECT", device: device}
], "DISCONNECT_COMPLETE").then(() => {
expect(store().getState().device).toBe(null);
})
});
It would be nice to turn this into Observable, however Promises work just fine in this case.
Hey, I'm not sure what you're asking?
Not really asking--- would like to just make a helpful post. Unsure of where to put it. Looking for input or any issues/improvements you may see
@connected-mgosbee ohh that makes more sense.
It seems generally good for many use cases, but won't work in all of them--particularly ones that redux-observable is commonly used for, where multiple actions are emitted over time.
Lately I've been recommending people test the epics directly, without a redux store, which so far has had positive responses and works well for me.
// api.js
// your API call helpers
import { ajax } from 'rxjs/observable/dom/ajax';
export const fetchSomething = id =>
ajax.getJSON(`/somethings/${id}`);
// the epic
export const somethingEpic = (action$, store, { api }) =>
action$.ofType(SOMETHING)
.mergeMap(action =>
api.fetchSomething(action.id)
.map(response => somethingFulfilled(response))
);
// inject your dependencies into all of your epics
// when making the root epic
import * as api from './api'
export const rootEpic = (...args) =>
combineEpics(somethingEpic, anotherEpic)(...args, { api });
// later to test, just mock the dependencies
const action$ = ActionsObservable.of({ type: SOMETHING, id: '123' });
const response = { id: '123', something: 'example' };
const api = { fetchSomething: id => Observable.of(response) };
somethingEpic(action$, null, { api })
.toArray()
.subscribe(actions => {
assertDeepEqual(actions, [
somethingFulfilled(response)
]);
});
@connected-mgosbee I also tend to use the very same approach described by @jayphelps and found out to be very easy to test this kind of stuff.
Closing because we have other tickets that are tracking the needed documentation updates.
Most helpful comment
@connected-mgosbee ohh that makes more sense.
It seems generally good for many use cases, but won't work in all of them--particularly ones that redux-observable is commonly used for, where multiple actions are emitted over time.
Lately I've been recommending people test the epics directly, without a redux store, which so far has had positive responses and works well for me.