How to unit test this Redux thunk? - javascript

So I have this Redux action creator that is using redux thunk middleware:
accountDetailsActions.js:
export function updateProduct(product) {
return (dispatch, getState) => {
const { accountDetails } = getState();
dispatch({
type: types.UPDATE_PRODUCT,
stateOfResidence: accountDetails.stateOfResidence,
product,
});
};
}
How do I test it? I'm using the chai package for testing. I have found some resources online, but am unsure of how to proceed. Here is my test so far:
accountDetailsReducer.test.js:
describe('types.UPDATE_PRODUCT', () => {
it('should update product when passed a product object', () => {
//arrange
const initialState = {
product: {}
};
const product = {
id: 1,
accountTypeId: 1,
officeRangeId: 1,
additionalInfo: "",
enabled: true
};
const action = actions.updateProduct(product);
const store = mockStore({courses: []}, action);
store.dispatch(action);
//this is as far as I've gotten - how can I populate my newState variable in order to test the `product` field after running the thunk?
//act
const newState = accountDetailsReducer(initialState, action);
//assert
expect(newState.product).to.be.an('object');
expect(newState.product).to.equal(product);
});
});
My thunk doesn't do any asynchronous actions. Any advice?

How to Unit Test Redux Thunks
The whole point of a thunk action creator is to dispatch asynchronous actions in the future. When using redux-thunk a good approach is to model the async flow of beginning and end resulting in success or an error with three actions.
Although this example uses Mocha and Chai for testing you could quite as easily use any assertion library or testing framework.
Modelling the async process with multiple actions managed by our main thunk action creator
Let us assume for the sake of this example that you want to perform an asynchronous operation that updates a product and want to know three crucial things.
When the async operation begins
When the async operation finishes
Whether the async operation succeeded or failed
Okay so time to model our redux actions based on these stages of the operation's lifecycle. Remember the same applies to all async operations so this would commonly be applied to http requests to fetch data from an api.
We can write our actions like so.
accountDetailsActions.js:
export function updateProductStarted (product) {
return {
type: 'UPDATE_PRODUCT_STARTED',
product,
stateOfResidence
}
}
export function updateProductSuccessful (product, stateOfResidence, timeTaken) {
return {
type: 'PRODUCT_UPDATE_SUCCESSFUL',
product,
stateOfResidence
timeTaken
}
}
export function updateProductFailure (product, err) {
return {
product,
stateOfResidence,
err
}
}
// our thunk action creator which dispatches the actions above asynchronously
export function updateProduct(product) {
return dispatch => {
const { accountDetails } = getState()
const stateOfResidence = accountDetails.stateOfResidence
// dispatch action as the async process has begun
dispatch(updateProductStarted(product, stateOfResidence))
return updateUser()
.then(timeTaken => {
dispatch(updateProductSuccessful(product, stateOfResidence, timeTaken))
// Yay! dispatch action because it worked
}
})
.catch(error => {
// if our updateUser function ever rejected - currently never does -
// oh no! dispatch action because of error
dispatch(updateProductFailure(product, error))
})
}
}
Note the busy looking action at the bottom. That is our thunk action creator. Since it returns a function it is a special action that is intercepted by redux-thunk middleware. That thunk action creator can dispatch the other action creators at a point in the future. Pretty smart.
Now we have written the actions to model an asynchronous process which is a user update. Let's say that this process is a function call that returns a promise as would be the most common approach today for dealing with async processes.
Define logic for the actual async operation that we are modelling with redux actions
For this example we will just create a generic function that returns a promise. Replace this with the actual function that updates users or does the async logic. Ensure that the function returns a promise.
We will use the function defined below in order to create a working self-contained example. To get a working example just throw this function in your actions file so it is in the scope of your thunk action creator.
// This is only an example to create asynchronism and record time taken
function updateUser(){
return new Promise( // Returns a promise will be fulfilled after a random interval
function(resolve, reject) {
window.setTimeout(
function() {
// We fulfill the promise with the time taken to fulfill
resolve(thisPromiseCount);
}, Math.random() * 2000 + 1000);
}
)
})
Our test file
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import chai from 'chai' // You can use any testing library
let expect = chai.expect;
import { updateProduct } from './accountDetailsActions.js'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('Test thunk action creator', () => {
it('expected actions should be dispatched on successful request', () => {
const store = mockStore({})
const expectedActions = [
'updateProductStarted',
'updateProductSuccessful'
]
return store.dispatch(fetchSomething())
.then(() => {
const actualActions = store.getActions().map(action => action.type)
expect(actualActions).to.eql(expectedActions)
})
})
it('expected actions should be dispatched on failed request', () => {
const store = mockStore({})
const expectedActions = [
'updateProductStarted',
'updateProductFailure'
]
return store.dispatch(fetchSomething())
.then(() => {
const actualActions = store.getActions().map(action => action.type)
expect(actualActions).to.eql(expectedActions)
})
})
})

Have a look at Recipe: Writing Tests from the official documentation. Also, what are you testing, the action creator or the reducer?
Action Creator Test Example
describe('types.UPDATE_PRODUCT', () => {
it('should update product when passed a product object', () => {
const store = mockStore({courses: []});
const expectedActions = [
/ * your expected actions */
];
return store.dispatch(actions.updateProduct(product))
.then(() => {
expect(store.getActions()).to.eql(expectedActions);
});
});
});
Reducer Test Example
Your reducer should be a pure function, so you can test it in isolation outside of the store environment.
const yourReducer = require('../reducers/your-reducer');
describe('reducer test', () => {
it('should do things', () => {
const initialState = {
product: {}
};
const action = {
type: types.UPDATE_PRODUCT,
stateOfResidence: // whatever values you want to test with,
product: {
id: 1,
accountTypeId: 1,
officeRangeId: 1,
additionalInfo: "",
enabled: true
}
}
const nextState = yourReducer(initialState, action);
expect(nextState).to.be.eql({ /* ... */ });
});
});

export const someAsyncAction = (param) => (dispatch, getState) => {
const { mock } = getState();
dispatch({
type: 'SOME_TYPE',
mock: mock + param,
})
}
it('should test someAsyncAction', () => {
const param = ' something';
const dispatch = jest.fn().mockImplementation();
const getState = () => ({
mock: 'mock value',
});
const expectedAction = {
type: 'SOME_TYPE',
mock: 'mock value something'
};
const callback = someAsyncAction(param);
expect(typeof callback).toBe('function');
callback.call(this, dispatch, getState);
expect(dispatch.mock.calls[0]).toEqual([expectedAction])
});

Related

Redux: Dispatch actions in sequence

I'm creating a Reddit client on Redux and I have 2 store dispatches firing in the app:
// App()
const dispatch = useDispatch();
useEffect(() => {
const stateMatch = window.location.href.match(/state=([^&]*)/);
const codeMatch = window.location.href.match(/code=([^&]*)/);
if ((stateMatch && codeMatch) || localStorage.getItem("access_token")) {
dispatch(fetchUser());
dispatch(fetchSubs());
}
});
...
However, I want fetchUser() to run and finish before fetchSubs() can begin, as the former currently seems to ruin API calls for the latter while it's running. How can I solve this?
Since you are using createAsyncThunk you can do something like this:
dispatch(fetchUser())
.unwrap()
.then((user) => {
// do anything you want with user, or don't, also dispatch actions
dispatch(fetchSubs());
})
.catch((e) => {
// error in case of rejection inside createAsyncThunk second argument
console.log(e);
});
Explanation
Let's say const thunk = fetchUser()
so basically dispatch(fetchUser()) is the same as dispatch(thunk).
Redux's dispatch function returns whatever its argument (the action) returns.
So in this case, dispatch(thunk) returns whatever thunk returns.
thunk, based on how createAsyncThunk works, returns a promise that either resolves to fulfilled action, or the rejected action. (those actions that you receive in extra reducers).
This is how you can access those actions:
dispatch(thunk).then(fullfilledAction=>...).catch(rejectedAction=>...`
RTK library also provides a method called unwrap. Instead of those action objects I explained above, it lets you use the returned value from the 2nd argument of createAsyncThunk.
export const fetchUser = createAsyncThunk("user/fetchUser", async () => {
const user = await Reddit.getUser().then(val => {
return val;
});
return user; // unwrap lets you use this instead of action objects.
})
Try this with pure react and redux hooks
...
const state = useStore(yourStore) //use your method to read state
const dispatch = useDispatch();
const checkValue = () => {
const stateMatch = window.location.href.match(/state=([^&]*)/);
const codeMatch = window.location.href.match(/code=([^&]*)/);
if ((stateMatch && codeMatch) || localStorage.getItem("access_token")) {
return true;
}
return false;
}
useEffect(() => {
if(checkValue())
dispatch(fetchUser());
}
}, []);
useEffect(() => {
if(checkValue() && state.authState)
dispatch(fetchSubs());
}
}, [state.authState]);
...

converting class to hooks getting Property 'then' does not exist on type '(dispatch: any) => Promise<void>'.ts(2339)

I'm new to react, here I have two same codes, one is with classes that work, and another is converted from that same class into hooks.
in hooks version, my 'then' is giving an error
Property 'then' does not exist on type '(dispatch: any) =>
Promise'.ts(2339)
have I made some mistake with conversion?
why it is not giving the same error in class while both are the same?
also console.log("Fetched model", realGraph.model); should give an object but it is giving undefined(in-class version it works), but if I put this console outside of loadGraph function then it gives an object, why it's not giving an object inside loadGraph function?
any ideas and suggestions?
class:
import { getGraph, getFloorplan, changeActiveCamera } from '../redux/actions';
const mapStateToProps = (state) => {
return {
currentSite: state.selection.currentSite,
currentCamera: state.selection.currentCamera,
};
};
function mapDispatchToProps(dispatch) {
return {
getGraph: (site) => dispatch(getGraph(site)),
getFloorplan: (site) => dispatch(getFloorplan(site)),
changeActiveCamera: (site, id) => dispatch(changeActiveCamera(site, id)),
};
}
loadGraph() {
if (this.props.currentSite) {
this.props.getFloorplan(this.props.currentSite.identif).then(() => {
console.log('Fetched floorplan');
this.props.getGraph(this.props.currentSite.identif).then(() => {
console.log('Fetched model', this.props.realGraph.model);
// new camera-related node & link status
if (this.props.currentCamera) {
this.props.changeActiveCamera(
this.props.currentSite.identif,
this.props.currentCamera.identif
);
}
});
});
}
}
converted from class to hooks:
Hooks:
const dispatch = useDispatch();
const realGraph = useSelector((state) => state.graphArticles.graph);
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const dispatchGetFloorplan = (site) => dispatch(getFloorplan(site));
const dispatchGetGraph = (site) => dispatch(getGraph(site));
const dispatchChangeActiveCamera = (site, id) =>
dispatch(changeActiveCamera(site, id));
const loadGraph = () => {
if (currentSite) {
dispatchGetFloorplan(currentSite.identif).then(() => {
console.log('Fetched floorplan');
dispatchGetGraph(currentSite.identif).then(() => {
console.log('Fetched model', realGraph.model);
// new camera-related node & link status
if (currentCamera) {
dispatchChangeActiveCamera(
currentSite.identif,
currentCamera.identif
);
}
});
});
}
};
my action related to those:
export function getGraph(site) {
return getData(`api/graph/${site}`, GET_GRAPHS);
}
export function getFloorplan(site) {
return getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
}
On first glance, there are several things I would change in the code you provided.
First, don't use any wrapper factories over your dispatch functions. Use dispatch(action()) directly where you need it component. You aren't gaining anything by creating wrapper functions.
Second, it would be advisable to use some sort of middleware, like Redux Thunk, to handle async Redux actions (like fetching something from the API).
The actions you provided are just "dumb" functions, which are not returning promises so you can't expect it to be "then"-able in your target component.
I also advise the async/await syntax since it is much more readable.
Third, you need to leverage the Hooks reactive API with the useEffect hook.
So first try to define getFloorPlan and getGraph as async actions using the redux-thunk syntax.
export const getGraphAsync = (site) => async (dispatch) => {
try {
const data = await getData(`api/graph/${site}`, GET_GRAPHS);
dispatch(saveGraphData(data)) // save data into Redux store with a normal, synchronous action (plain object)
} catch (error) {
console.log(error)
}
}
export const getFloorplanAsync = (site) => async (dispatch) => {
try {
const data = await getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
dispatch(saveImageData(data)) // save data into Redux store with a normal, synchronous action (plain object)
} catch (error) {
console.log(error)
}
}
I am making an assumption that you correctly configured your store.js to use the thunk middleware.
And then refactor the rest of the component (following some best practices):
const someHookComponent = () => {
// ...
const dispatch = useDispatch();
const currentSite = useSelector((state) =>
state.selection.currentSite);
const currentCamera = useSelector((state) =>
state.selection.currentCamera);
const loadGraph = async () => {
if (currentSite) {
await dispatch(getFloorPlanAsync(currentSite.identif));
console.log('Fetched floorplan');
await dispatch(getGraphAsync(currentSite.identif));
console.log('Fetched model', realGraph.model); /* where is
realGraph coming from? */
/* Why is it important that these 2 dispatches follow one
another when there is no data being passed from one to the
other, or being used later in the component... */
});
});
}
};
useEffect(() => {
// new camera-related node & link status
if (currentCamera) {
dispatch(changeActiveCamera(
currentSite.identif,
currentCamera.identif
));
}
}, [currentSite?.identif, currentCamera?.identif]) /* null chaining is optional here */
// ...
}
I am guessing that loadGraph gets called by some onClick event somewhere down the line like this:
onClick={loadGraph}
If it is called inside useEffect, define the deps (variables used inside loadGraph):
useDeepCompareEffect(() => {
// ... some logic
loadGraph()
}, [currentSite, realGraph])
If you put your currentSite and currentCamera objects directly into the useEffect list of deps then you need to do a deep comparison "by hand".
In that case it's best to create a custom hook like useDeepCompareEffect which will do the heavy lifting of running deep comparisons of reference types under the hood (with the help of some library like lodash for example).
If you want to use or console.log the latest value of realGraph (reference type), you need to use the useEffect hook with a deep comparison again (or just extract the target primitive directly into the deps list and use vanilla useEffect) :
useDeepCompareEffect(() => {
if (realGraph) {
console.log('Fetched model', realGraph.model);
}
}, [realGraph]) // reference type
// or
useEffect(() => {
if (realGraph) {
console.log('Fetched model', realGraph.model);
}
}, [realGraph.someProperty]) // primitive

Async does wait for data to be returned in a redux-thunk function

I've being trying populate my redux store with data that comes from my mongo-db realm database.
Whenever I run the function below it will execute fine but the problem is data will be delayed and ends up not reaching my redux store.
My thunk function:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
try {
return await Realm.open(config).then(async (projectRealm) => {
let syncItems = await projectRealm.objects("Item");
await syncItems.addListener((x, changes) => {
x.map(async (b) => {
console.log(b);
return b;
});
});
});
} catch (error) {
console.log(error);
throw error;
}
}
);
and my redux reducer:
extraReducers: (builder) => {
builder.addCase(addItemsTest.fulfilled, (state, { payload }: any) => {
try {
console.log("from add Items");
console.log(payload);
state.push(payload);
} catch (error) {
console.log(error)
}
});
}
Expected Results:
My redux store should have these data once addItemsTest return something:
[{
itemCode: 1,
itemDescription: 'Soccer Ball',
itemPrice: '35',
partition: 'partitionValue',
},
{
itemCode: 2,
itemDescription: 'Base Ball',
itemPrice: '60',
partition: 'partitionValue',
}
]
Actual Results:
Mixed Syntaxes
You are combining await/async and Promise.then() syntax in a very confusing way. It is not an error to mix the two syntaxes, but I do not recommend it. Stick to just await/async
Void Return
Your action actually does not return any value right now because your inner then function doesn't return anything. The only return is inside of the then is in the x.map callback. await syncItems is the returned value for the mapper, not for your function.
Right now, here's what your thunk does:
open a connection
get items from realm
add a listener to those items which logs the changes
returns a Promise which resolves to void
Solution
I believe what you want is this:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
try {
const projectRealm = await Realm.open(config);
const syncItems = await projectRealm.objects("Item");
console.log(syncItems);
return syncItems;
} catch (error) {
console.log(error);
throw error;
}
}
);
Without the logging, it can be simplified to:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
const projectRealm = await Realm.open(config);
return await projectRealm.objects("Item");
}
);
You don't need to catch errors because the createAsyncThunk will handle errors by dispatching an error action.
Edit: Listening To Changes
It seems that your intention is to sync your redux store with changes in your Realm collection. So you want to add a listener to the collection that calls dispatch with some action to process the changes.
Here I am assuming that this action takes an array with all of the items in your collection. Something like this:
const processItems = createAction("processItems", (items: Item[]) => ({
payload: items
}));
Replacing the entire array in your state is the easiest approach. It will lead to some unnecessary re-renders when you replace item objects with identical versions, but that's not a big deal.
Alternatively, you could pass specific properties of the changes such as insertions and handle them in your reducer on a case-by-case basis.
In order to add a listener that dispatches processItems, we need access to two variables: the realm config and the redux dispatch. You can do this in your component or by calling an "init" action. I don't think there's really much difference. You could do something in your reducer in response to the "init" action if you wanted.
Here's a function to add the listener. The Realm.Results object is "array-like" but not exactly an array so we use [...x] to cast it to an array.
FYI this function may throw errors. This is good if using in createAsyncThunk, but in a component we would want to catch those errors.
const loadCollection = async (config: Realm.Configuration, dispatch: Dispatch): Promise<void> => {
const projectRealm = await Realm.open(config);
const collection = await projectRealm.objects<Item>("Item");
collection.addListener((x, changes) => {
dispatch(processItems([...x]));
});
}
Adding the listener through an intermediate addListener action creator:
export const addListener = createAsyncThunk(
"init",
async (config: Realm.Configuration, { dispatch }) => {
return await loadCollection(config, dispatch);
}
);
// is config a prop or an imported global variable?
const InitComponent = ({config}: {config: Realm.Configuration}) => {
const dispatch = useDispatch();
useEffect( () => {
dispatch(addListener(config));
}, [config, dispatch]);
/* ... */
}
Adding the listener directly:
const EffectComponent = ({config}: {config: Realm.Configuration}) => {
const dispatch = useDispatch();
useEffect( () => {
// async action in a useEffect need to be defined and then called
const addListener = async () => {
try {
loadCollection(config, dispatch);
} catch (e) {
console.error(e);
}
}
addListener();
}, [config, dispatch]);
/* ... */
}

Unit testing Redux async actions

I am trying to add unit test cases to my redux actions.
I have tried this, this & this
I am using thunk, promise-middleware in my actions
one of my action is like this
export function deleteCommand(id) {
return (dispatch) => {
dispatch({
type: 'DELETE_COMMAND',
payload: axios.post(`${Config.apiUrl}delete`, { _id: id })
})
}
}
unit test for this is
import configureMockStore from "redux-mock-store"
const middlewares = [thunk, promiseMiddleware()];
const mockStore = configureMockStore(middlewares)
it('creates DELETE_COMMAND_FULFILLED after deleting entry', (done) => {
nock('http://localhost:8081/')
.post('/delete',{
_id: 1
})
.reply(200, {})
const expectedActions = [{
type: 'DELETE_COMMAND_FULFILLED',
payload: {}
}]
const store = mockStore({
command: {
commands: []
}
})
store.dispatch(actions.deleteCommand({_id: 1}).then(function () {
expect(store.getActions())
.toEqual(expectedActions)
done();
})
})
I am using nock,redux-mock-store configured with thunk, promise middleware
It gives then of undefined
then I changed the action to return the promise, then I got unhandled promise reject exception, I added a catch to the action dispatch call.
Now I am getting Network Error because nock is not mocking the call.
Also tried moxios, changed axios to isomorphic-fetch, whatwg-fetch. Doesn't seem to be working
Where am I doing it wrong?.
I know It's from 2017,
But maybe someone will have the same issue.
So the problem is that the arrow function inside deleteCommand returns undefined.
That's why you get
It gives then of undefined
TLDR:
if you using redux-thunk with redux-promise-middleware
You must return the inner dispatch
here is the official redux-promise-middleware docs
The solution is very simple :
Option 1.
Add return inside the arrow function
export function deleteCommand(id) {
return (dispatch) => {
return dispatch({
type: 'DELETE_COMMAND',
payload: axios.post(`${Config.apiUrl}delete`, { _id: id })
})
}
}
Option 2.
Remove the curly braces from the arrow function
export function deleteCommand(id) {
return (dispatch) =>
dispatch({
type: 'DELETE_COMMAND',
payload: axios.post(`${Config.apiUrl}delete`, { _id: id })
})
}
Now you can do
store.deleteCommand(<some id>).then(...)
In general arrow functions and return
Return a plain object
// option 1
const arrowFuncOne = () => {
return {...someProps}
}
// option 2
const arrowFuncTwo = () => ({
...someProps
})
// This means that you returning an expression
//same as
// return a result of other function
const arrowFuncCallFunOne = () => callSomeOtherFunc()
// or
const arrowCallFunkTwo = () => {return callSomeOtherFunc()}
// However
// wrong
const arrowCallFunkNoReturn = () => {callSomeOtherFunc()}
// in this case the curly braces are just the body of the function
// and inside this body there is no return at all

Why can't I dispatch an action when a promise resolves inside Redux middleware?

Background
I am writing a piece of Redux middleware that makes an axios request and uses Cheerio to parse the result.
Problem
When the Axios promise resolves and I try to dispatch a fulfilled action the action does not show up in the store's action log in the test.
Middleware
function createMiddleware() {
return ({ dispatch, getState }) => next => action => {
if (isCorrectAction(action)===true){
const pendingAction = {
type: `${action.type}_PENDING`,
payload: action.payload
}
dispatch(pendingAction)
axios.get(action.payload.url)
.then(response => {
let $ = cheerio.load(response.data)
let parsedData = action.payload.task($)
const fulfilledAction = {
type: `${action.type}_FULFILLED`,
payload: {
parsedData
}
}
dispatch(fulfilledAction) // dispatch that is problematic
})
.catch( err => {
});
}
return next(action);
}
}
Test that fulfilled action is dispatched fails
it('should dispatch ACTION_FULFILLED once', () => {
nock("http://www.example.com")
.filteringPath(function(path) {
return '/';
})
.get("/")
.reply(200, '<!doctype html><html><body><div>text</div></body></html>');
const expectedActionTypes = ['TASK', 'TASK_PENDING', 'TASK_FULFILLED']
// Initialize mockstore with empty state
const initialState = {}
const store = mockStore(initialState)
store.dispatch(defaultAction)
const actionsTypes = store.getActions().map(e => e.type)
expect(actionsTypes).has.members(expectedActionTypes);
expect(actionsTypes.length).equal(3);
});
Solution - Promise needs to be returned in the mocha test
The solution is to rewrite the mocha test so that the promise is returned. I mistakenly thought that by using nock to intercept the HTTP request that the promise would become synchronous.
The working test looks like:
it('should dispatch ACTION_FULFILLED once', () => {
nock("http://www.example.com")
.filteringPath(function(path) {
return '/';
})
.get("/")
.reply(200, '<!doctype html><html><body><div>text</div></body></html>');
const store = mockStore();
return store.dispatch(defaultScrapingAction)
.then(res => {
const actionsTypes = store.getActions().map(e => e.type)
expect(actionsTypes).has.members(expectedActionTypes);
expect(actionsTypes.length).equal(3);
})
});

Categories