Hello 馃憢
I spent a lot of time reading the documentation and examples:
Testing simple reducers is extremely easy and straightforward, but I could not find any indication on how to test extraReducers and in particular createAsyncThunk. Did I miss something? Or are those features I should not test?
Can I contribute to the docs somehow regarding this issue?
for some context, here is my slice:
const addPerson = createAsyncThunk(
'family/createPerson',
async (payload: { person: Person }) => {
const _id = nanoid();
const newPayload = {
_id,
person: payload.person,
};
return newPayload;
},
);
const familySlice = createSlice({
name: 'family',
initialState,
reducers: {
removePerson: (/* ... */) => {
// ...
},
editPerson(/* ... */) {
// ...
},
},
extraReducers: (builder) => {
builder.addCase(addPerson.fulfilled, (state, action) => {
const { person, _id } = action.payload;
state.personsCreated++;
state.persons[_id] = person;
});
},
});
_sidenote: if it feels unnecessary, I am creating a thunk because I need to get back the person ID in order to do a redirection based on this ID._
All the case reducers in both the reducers and extraReducers fields will get added into familySlice.reducer. So, you can test that logic as per any other Redux reducer:
const actual = someReducer(testState, testAction);
expect(actual).toEqual(expectedState);
It's up to you how you generate testState, testAction, and expectedState, including either writing the actions by hand or using action creators to generate them.
So for other people reading the issue, it looks like what I was looking for is the "sub-actions" created by createAsyncThunk + https://github.com/reduxjs/redux-toolkit/issues/494
const action = familyActions.addPerson.fulfilled( // <-- THIS
{
person: person1,
_id: 'someRandomString',
},
'',
{ person: person1 },
);
expect(familySlice.reducer(someInitialState, action)).toStrictEqual({
// ...
});
@martpie, can you elaborate on what parameters you're passing to familyActions.addPerson.fulfilled?
So from what I understand, .fullfilled create an action creator based on the data you passed to it.
The TS Typings and Docs are really unclear at this subject.
Person with an additional _id property)ThunkArg, again, I have no idea what it does, but it was typed for me already.Sorry, I am not of a big help, but some internals of the toolkit are quite hard to get.
If you take a look at the source, it's a createAction call with a prepare argument:
So the signature of the final action creator is:
function fulfilled(result: Returned, requestId: string, arg: ThunkArg): {
type: string
payload: Returned
meta: { arg: ThunkArg, requestId: string }
}
Where arg is what you initially passed into the asyncThunk and requestId is a string that is unique for the whole pending/fulfilled/rejected triplet.
If your reducer is not really using these, just pass in anything.
When in doubt, read the source :)
Here is a way you could test the fulfilled sub-action of your family/createPerson action.
import reducer from 'path/to/your/family/slice'
const addPersonPayload = {
person: person1,
_id: 'someRandomString'
}
const addPersonArg = {
person: person1
}
// Mock/Stub your addPerson function to return/resolve the addPersonPayload when called with addPersonArg as its argument
// I used Cypress, you can use whatever tool to mock/stub your function
// Here's how it would look like with Cypress
const addPersonStub = cy.stub({addPerson}, 'addPerson'}).withArgs(addPersonArg).resolves(addPersonPayload)
const nextState = reducer(
someInitialState,
store.dispatcher({
type: 'family/createPerson/fulfilled',
payload: await addPersonStub(addPersonArg)
})
)
const rootState = {family: nextState}
expect(someSelector(rootState)).to.equal(addPersonPayload) // Chai's to.equal syntax that Cypress uses
Most helpful comment
So for other people reading the issue, it looks like what I was looking for is the "sub-actions" created by
createAsyncThunk+ https://github.com/reduxjs/redux-toolkit/issues/494