Redux - Jest: Testing functions that have void return - redux

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.

Related

Redux-Toolkit Middleware custom: Uncaught RangeError: Maximum call stack size exceeded

I am studying Redux-Toolkit studio, and in particular, I am approaching custom middleware.
Below I report the definition of my store within my classic application example:
import { combineReducers, configureStore} from '#reduxjs/toolkit';
import {todosSlice} from '.. /features/todos/todosSlice';
import { filterStore } from '.. /features/todos/filterSlice';
import { myLog } from '.. /reduxtoolkitmiddlewarecustom/myLog';
const rootReducer = combineReducers({
todos: todosSlice.reducer,
filter: filterStore.reducer
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware(). concat(myLog),
});
As you see in the store amount, the middleware "custom" defined in '.. /reduxtoolkitmiddlewarecustom/myLog';
The definition of middleware is as follows:
export const myLog = (store) => {
console.log('Init myLog');
return function myDispatch(next) {
return function myAction(action) { //action is the action received from the store.
store.Dispatch({ type: 'INIT_MYLOG', payload: null });
console.log(action);
//return next(action);
}
}
};
Well, if I emit a Dispatch, I do not return next(action), is that the return I still get an error of "Uncaught Rangeerror: Maximum call stack size exceeded".
I thought I understood, but evidently, I don't. The only way to make the code work is, for example, to make a console.log
What am I tripping about?
If found the error, in general, if the middleware makes a Dispatch in the store, must a corresponding reducer/action creator exist?
Thank you so much
your middleware dispatches INIT_MYLOG on every action. and that dispatched INIT_MYLOG then triggers your middleware. and that then dispatches. an endless circle.
Solution? Don't dispatch in a middleware except if you do it as a reaction to one specific action.

How to test nested firestore batch functions in redux saga using jest?

In a react project I have a redux-saga file which I create and save new items on firebase backend(firestore).
In that saga function, I am getting a new write batch object first, then I update the firestore document, and finally I commit the batch.
Saga Worker
import { call, put } from 'redux-saga/effects'
import { db } from './firebase' // db: firebase.firestore()
export function* mySaga(item) {
try {
// init firestore batch.
const batch = yield call(db, db.batch)
// Set firestore document and save new item.
const itemRef = yield call ([db, db.doc], `/items/${item.id}`)
yield call([batch, batch.set], itemRef , item)
// Commit the batch.
yield call([batch, batch.commit])
yield put({type: 'success'})
} catch (err) {
yield put({type: 'error', payload: err})
}
}
Saga Worker's Test
import * as sagas from './mySaga'
describe('mySaga', () => {
const spyOnDoc = jest.spyOn(db, 'doc')
it('handles item creation', async () => {
const dispatched = []
await runSaga(
{ dispatch: action => dispatched.push(action) },
sagas.mySaga,
).toPromise()
expect(spyOnDoc).toHaveBeenCalledTimes(1)
// !!! Here I need to check for nested set and commit functions of the batch object created in saga.
})
})
How can I test the batch function's nested "set" and "commit" functions to check if they are called x times and called with proper inputs?
Any help would be appreciated.
After several attemps I figured out a way to accomplishing this kind of tests. In case if someone needs this solution, here it is.
db.batch() method creates a firebase.firestore.WriteBatch object. And this object has commit, set, update and delete methods. More details can be found here.
Final Saga Worker's Test
import * as sagas from './mySaga'
import { db } from './firebase' // db: firebase.firestore()
describe('mySaga', () => {
const spyOnDoc = jest.spyOn(db, 'doc')
// We are mocking the methods of this predefined object.
firestore.WriteBatch.set = jest.fn()
firestore.WriteBatch.commit = jest.fn()
// Then we implement those created mocks into the batch's mock implementation.
const spyOnBatch = jest.spyOn(db, 'batch').mockImplementation(() => ({
set: firestore.WriteBatch.set,
commit: firestore.WriteBatch.commit,
}))
it('handles item creation', async () => {
const dispatched = []
await runSaga(
{ dispatch: action => dispatched.push(action) },
sagas.mySaga,
{id: 123} // Item
).toPromise()
expect(spyOnDoc).toHaveBeenCalledTimes(1)
// Finally, we can test those nested object functions as below.
expect(firestore.WriteBatch.commit).toHaveBeenCalledTimes(1)
expect(firestore.WriteBatch.set).toHaveBeenCalledTimes(1)
expect(firestore.WriteBatch.set).toHaveBeenCalledWith(db.doc('/items/123'), {id: 123})
})
})

Testing async mapDispatchToProps actions with Jest/Enzyme gives error

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!

Perform Ajax Fetch in a Redux Reducer?

I'm trying to wrap my head around accessing the state inside Redux actionCreators; instead did the following (performed ajax operation in the reducer). Why do I need to access the state for this — because I want to perform ajax with a CSRF token stored in the state.
Could someone please tell me if the following is considered bad practice/anti-pattern?
export const reducer = (state = {} , action = {}) => {
case DELETE_COMMENT: {
// back-end ops
const formData = new FormData();
formData.append('csrf' , state.csrfToken);
fetch('/delete-comment/' + action.commentId , {
credentials:'include' ,
headers:new Headers({
'X-Requested-With':'XMLHttpRequest'
}) ,
method:'POST' ,
body:formData
})
// return new state
return {
...state ,
comments:state.comments.filter(comment => comment.id !== action.commentId)
};
}
default: {
return state;
}
}
From the redux documentation:
The only way to change the state is to emit an action, an object describing what happened. Do not put API calls into reducers. Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state.
Actions should describe the change. Therefore, the action should contain the data for the new version of the state, or at least specify the transformation that needs to be made. As such, API calls should go into async actions that dispatch action(s) to update the state. Reducers must always be pure, and have no side effects.
Check out async actions for more information.
An example of an async action from the redux examples:
function fetchPosts(subreddit) {
return (dispatch, getState) => {
// contains the current state object
const state = getState();
// get token
const token = state.some.token;
dispatch(requestPosts(subreddit));
// Perform the API request
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
// Then dispatch the resulting json/data to the reducer
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
As per guidelines of redux.
It's very important that the reducer stays pure. Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
If you are asking whether it is anti-pattern or not then yes it is absolutely.
But if you ask what is the solution.
Here you need to dispatch async-action from your action-creators
Use "redux-thunk" or "redux-saga" for that
You can access the state and create some async action
e.g inside your action-creator ( Just for example )
export function deleteCommment(commentId) {
return dispatch => {
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}
export function updateCommentList(commentList) {
return {
type : UPDATE_COMMENT_LIST,
commentList
};
}
Edit: You can access the state -
export function deleteCommment(commentId) {
return (dispatch, getState) => {
const state = getState();
// use some data from state
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}

Redux Saga not triggering API call when action is dispatched

I've used redux saga before but i'm still fairly new to it. Anyways, I seem to be running into a problem in the code below.
At the top, you will see the action creator I am using to fire off this AJAX request. Redux is properly dispatching this action and is logging the LOAD_USER_REQUEST type in my console however the function chain stops there. In the code below, you will see that LOAD_USER_REQUEST should call the loadUserDetails generator which should then call the userLogin with the payload received in my action creator.
Let me know if I can supply any additional info that may help. Thanks in advance :)
// Action Creator for LOAD_USER_REQUEST.
export const getExistingUser = ({email = 'tt#gmail.com', password = '12345'} = {}) => ({
type: LOAD_USER_REQUEST,
payload: {email, password}
})
// API call being used in loadUserDetails Saga
export const userLogin = ({email = 'tt#gmail.com', password = '12345'} = {}) => {
return axios.post(`${API}auth/login`, {
email,
password
})
.then(res => {
console.log(res);
localStorage.setItem('token', res.data.token);
let user = res.data.user;
console.log(user);
return user;
})
.catch(err => new Error('userLogin err', err));
}
// Sagas
// loadUserDetails Saga - Should call fn above userLogin with payload from action creator
function* loadUserDetails(payload) {
const user = yield call(userLogin(payload));
yield put({type: LOAD_USER_SUCCESS, user}); // Yields effect to the reducer specifying the action type and user details
}
export function* watchRequest() {
yield* takeLatest(LOAD_USER_REQUEST, loadUserDetails);
}
At first, does your entry point to saga configured well? You should add saga-middleware in store creation, and don't forget to invoke saga process manager by runSaga method.
At second, why you re-delegate generator instance to up-level? Maybe it's meant to yield takeLatest(LOAD_USER_REQUEST, loadUserDetails); without yield* operator? Is has quite different semantics.
At third, by API reference, call effect takes function or generator reference, but you provide promise object. Maybe it's meant const user = yield call(() => userLogin(payload)); or const user = yield call(userLogin, payload);?

Resources