Testing async mapDispatchToProps actions with Jest/Enzyme gives error - asynchronous

I am trying to test my mapDispatchToProps actions when an async function is dispatched. I almost tried every possible solution I found and nothing worked so far. I'm always getting the same error:
I'm getting this error:
TypeError: store.dispatch(...).then is not a function
I tried the solution included in redux-mock-store https://github.com/dmitry-zaets/redux-mock-store. I included my middlewares to my mockStore, but it didn't fix the issue.
I tried the solution proposed by Michael Peyper here Testing dispatched actions in Redux thunk with Jest.
We created a function to build the mockStore so I tried to create my mockStore directly within my test file instead, but they both returned the same error.
I can't put all the solutions I tried here because it would take me weeks, but it gives you an idea.
Here's the code for my test:
describe('Component async actions', () => {
const middlewares = [thunk, queryMiddleware];
const createMockStore = configureStore(middlewares);
const store = createMockStore();
afterEach(() => {
jest.clearAllMocks();
});
const someData = {};
const expectedActions = {
type: ADD_DATA,
payload: someData
};
it('should handle addData', () => {
return store.dispatch(actions.addData(someData)).then(() => {
expect(store.getActions()[0]).toEqual(expectedAction);
});
});
});
Here's my mapDispatchToProps:
function mapDispatchToProps(dispatch) {
return {
addData: data => dispatch(addData(data))
.then(({ status }) => {
dispatch(showNotification({ status }));
}),
};
};
I would like to at least be able to get to the expect part and fix this if there's any error in my test, but I can't get passed the dispatch().then
Again, here's the error I get each time: TypeError: store.dispatch(...).then is not a function
Thanks in advance!

I don't know if anyone will get this problem, but I found a solution.
First of all, I had to add my thunk middleware to my createStore from redux-mock-store.
import thunk from 'redux-thunk';
...
const createMockStore = createStore([thunk]);
Then I did a mock of my addData function like this:
import { addData } from 'path/to/addData';
...
jest.mock('path/to/addData');
and I added this code within my test:
addData.mockReturnValue(() =>
new Promise((resolve) => resolve({ status: 200 }));
));
It works!

Related

Difficulty implementing redux thunk

This is my first time working with redux hooks and I keep receiving the error: "Error: Actions must be plain objects. Use custom middleware for async actions."
I have added the middleware thunk. Following the other peoples questions, I am not sure where I am making the mistake. I'm looking for an explanation on what I am doing wrong and what I should be reading in order to fix it.
Actions:
export const fetchNewsData = () => {
return (dispatch) => {
axios.get('http://localhost:3001/getnews')
.then(response => {
console.log(response.data);
const data = response.data;
dispatch(loadNews(data));
})
.catch(error => {
console.log(error);
dispatch(errorOnNews(error));
});
}
}
export const loadNews = (fetchedData) => {
return {
type: LOAD_NEWS,
payload: fetchedData
}
}
export const errorOnNews = (errorMessage) => {
return {
type: ERROR_ON_NEWS,
payload: errorMessage
}
}
Reducer:
const initialState = {
fetched: false,
data: [],
input: '',
filtered: [],
error: ''
}
const newsReducer = (state = initialState, action) => {
switch(action.type) {
case LOAD_NEWS:
return {
...state,
fetched: true,
data: action.payload
}
case FILTER_NEWS:
return {
...state
}
case ERROR_ON_NEWS:
return {
...state,
error: action.payload
}
default: return state;
}
}
Store:
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import rootReducer from './rootReducer';
const store = createStore(rootReducer, applyMiddleware(thunk));
Component:
const fetch = useDispatch(fetchNewsData());
useEffect(() => {
if(hasFetched){
// work on true condition
} else {
fetch(); // fails on this line.
}
}, []);
useDispatch does not work like this, as it ignores all arguments and just returns you a dispatch function. So you have called dispatch() there, which essentially equals dispatch(undefined) - and the store doesn't know what to make of that action.
Do this instead:
const dispatch = useDispatch();
useEffect(() => {
if(hasFetched){
// work on true condition
} else {
dispatch(fetchNewsData()); // fails on this line.
}
}, []);
Also, generally you are writing a very outdated style of redux here that we do not really recommend to learn or use in new applications any more.
You might have been following an outdated tutorial - as this style requires you to write multiple times the necessary code and is much more error prone.
For up-to-date tutorials featuring modern redux with the official redux toolkit please see the official redux tutorials
Was following a tutorial and creating additional work that was unnessessary. Answer is to:
Cut: const fetch = useDispatch(fetchNewsData());
Change: fetch(); to fetchNewsData();
In this case, I am calling a handling function that will execute the dispatches when required.

Redux testing: Actions must be plain objects. Use custom middleware for async actions

I have a Redux app and it is working perfectly without any errors. Now I am trying to test it with Enzyme, Jest and Sinon:
it('calls constructor', () => {
sinon.spy(SavedVariantsComponent.prototype, 'constructor')
const store = configureStore()(STATE1)
wrapper = mount(<SavedVariantsComponent store={store} match={{ params: {} }} />)
expect(SavedVariantsComponent.prototype.constructor).toHaveProperty('callCount', 1)
})
In SavedVariantsComponent I have mapDispatchToProps:
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onSubmit: (updates) => {
dispatch(updateSavedVariantTable(updates))
const { match, analysisGroup } = ownProps
const { familyGuid, variantGuid, tagArray, gene } = match.params
const familyGuids = familyGuid ? [familyGuid] : (analysisGroup || {}).familyGuids
const combineVariants = /combined_variants/.test(match.url)
dispatch(loadSavedVariants(combineVariants, familyGuids, variantGuid, tagArray, gene))
},
loadSavedVariants: (...args) => dispatch(loadSavedVariants(...args)),
}
}
And loadSavedVariants look like that:
export const loadSavedVariants = (combineVariants, familyGuids, variantGuid, tagArray, gene = '') => {
return (dispatch, getState) => {
...
...
and the error while running jest is:
Actions must be plain objects. Use custom middleware for async actions.
Which makes an HTTP Request that may not work in the current case. How to fix this error? I need to test that the constructor was called, but later on will also need to see how the inner Components are rendered, so need to have mount there. I suppose I am doing something wrong in testing and not in the real code since the latter is working without any errors, warnings or issues.
You probably need to configure your mock store to work with redux-thunk. See: https://github.com/dmitry-zaets/redux-mock-store#asynchronous-actions
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk] // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares)

redux-observable use RxJS to emit progress actions for ajax call

I have been wrestling with this problem and feel like I have a fundamental misunderstanding. I am using the redux-observable library in React which glues redux together with RxJS for handling asynchrony. My problem is that I have to handle a large upload and I want to show progress as the file is loaded.
The function uploadFileEpic needs to return an Observable<Action> to work with redux-observable. The uploadObservable represents the workflow that I want to accomplish. If I just return the uploadObservable the upload works but I don't get any handleUploadFileProgress actions from the progressSubscriber in the ajax call. Ideally the progressSubscriber would be adding elements to another observable that I could merge with uploadObservable. You see me trying to use merge here but the TypeScript compiler complains saying the return is not assignable to an ObservableInput.
I keep going in circles so I feel my understanding must be fundamentally off. I feel like I'm missing some simple RxJS magic here. Thanks for the help!
import { Observable, Observer, Subscriber, Subject, of } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { ofType } from 'redux-observable';
import { catchError, delay, map, mergeMap, tap, merge } from 'rxjs/operators';
import { apis } from '../../config';
export const enum ActionType {
InitialFileUpload
FileProgress
UploadFileSuccess
UploadFileFail
}
const handleInitialFileUpload = (file: File, timeLimit: number) => ({
type: ActionType.InitialFileUpload,
file,
timeLimit
})
const handleFileProgress = (file: File, percentComplete: number) => ({
type: ActionType.FileProgress,
file,
percentComplete
})
const handleUploadFileSuccess = (file: File, timeLimit: number) => ({
type: ActionType.UploadFileSuccess,
file,
timeLimit
})
const handleUploadFileFail = (file: File, timeLimit: number) => ({
type: ActionType.UploadFileFail,
file,
timeLimit
})
export const uploadFileEpic= action$ =>
action$.pipe(
ofType(ActionType.InitialFileUpload),
mergeMap((action: any) => {
const { file, timeLimit } = action;
const data = new FormData()
data.append('importFile', file, file.name)
data.append('timeLimit', timeLimit)
const progressSubject = new Subject();
const ajaxRequest = {
url: apis.gateway.run,
method: 'POST',
body: data,
headers: {},
progressSubscriber: Subscriber.create(
(e: ProgressEvent) => {
const percentComplete = Math.round((e.loaded / e.total) * 100)
console.log("Progress event")
progressSubject.next(handleUploadFileProgress(file, percentComplete))
}
)
}
const uploadObservable = ajax(ajaxRequest)
.pipe(
map(res => handleUploadFileSuccess(file)),
delay(SUCCESSFUL_UPLOAD_NOTIFICATION_LENGTH),
map(() => handleUploadFileRemove(file)),
catchError(error => of(handleUploadFileFail(file, error.response)))
)
return merge(uploadObservable, progressSubject)
}
)
)
You seem to be importing merge from rxjs/operators. There, merge is treated as an operator and thus returning an OperatorFunction. By importing from simply rxjs you get the static merge that correctly returns an Observable which will be flattened by your mergeMap.

Redux - Jest: Testing functions that have void return

New to Jest and Redux and I'm having trouble with testing functions that are dispatching to the store but don't yield a return value. I'm trying to follow the example from the Redux website does this
return store.dispatch(actions.fetchTodos()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
however I have several "fetchtodos" functions that don't return anything which causes the error TypeError:
Cannot read property 'then' of undefined due to returning undefined
I'm wondering what I can do to test that my mock store is correctly updating. Is there a way to dispatch the function, wait for it to finish and then compare the mock store with expected results?
Thanks
Edit: We're using typescript
action from tsx
export function selectTopic(topic: Topic | undefined): (dispatch: Redux.Dispatch<TopicState>) => void {
return (dispatch: Redux.Dispatch<TopicState>): void => {
dispatch({
type: SELECT_Topic,
payload: topic,
});
dispatch(reset(topic));
};
}
test.tsx
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('Select Topic action', () => {
it('should create an action to select .', () => {
const topic: Topic = mockdata.example[0];
const expectedAction = {
type: actions.SELECT_TOPIC,
payload: topic,
};
const store = mockStore(mockdata.defaultState);
return store.dispatch(actions.selectTopic(topic)).then(() => {
expect(store.getState()).toEqual(expectedAction);
});
});
});
The action is what I'm given to test(and there are many other functions similar to it. I'm getting that undefined error when running the test code, as the function isn't returning anything.
In Redux, the store's dispatch method is synchronous unless you attach middleware that changes that behavior, ie: returns a promise.
So this is likely a redux configuration problem. Be sure you are setting up your test store with the same middleware that allows you to use the promise pattern in production.
And as always, be sure to mock any network requests to avoid making api calls in test.

What is the second argument for?

I find this code in a tutorial
...
import configureMockStore from 'redux-mock-store';
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
...
it('should create BEGIN_AJAX_CALL & LOAD_COURSES_SUCCESS', (done) => {
const expectedActions = [
{type: types.BEGIN_AJAX_CALL},
{type: types.LOAD_COURSES_SUCCESS, body: {
courses: [{id:'clean-code', title:'Clean Code'}]
}}
];
const store = mockStore({courses:[]}, expectedActions);
store
.dispatch(courseActions.loadCourses())
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(types.BEGIN_AJAX_CALL);
expect(actions[1].type).toEqual(types.LOAD_COURSES_SUCCESS);
done();
});
});
and the whole bit with expectedActions doesn't make sense.
The docs say that if there is a second argument to store, it should be a function; (no explanation telling what that function would do though).
At first I thought it was forcing some actions into the store for some reason, but a quick console.log told me that wasn't the case.
Because only dispatch causes actions to accumulate.
So is it a mistake in the text or some wisdom to explore further?
This feature was removed in version 1, but you can find the example in the pre 1 docs.
The parameter expectedActions is used for testing. You can create a mock store with an array of actions, and then dispatch an the 1st action. This action will cause the other other actions to forwarded (dispatch / next) via thunks/api middleware/etc... The test will check if all of the actions in the expectedActions array have acted on the store:
import configureStore from 'redux-mock-store';
const middlewares = []; // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares);
// Test in mocha
it('should dispatch action', (done) => {
const getState = {}; // initial state of the store
const action = { type: 'ADD_TODO' };
const expectedActions = [action];
const store = mockStore(getState, expectedActions, done);
store.dispatch(action);
})

Resources