Best practices when writing tests for redux-saga with modular selectors
chawax opened this issue · comments
In the playground selectors examples we use the state exported from a reducer :
For example :
import { CountersState } from './reducer';
export const getReduxCounter = (state: CountersState) => state.reduxCounter;
But if I am not wrong, when you call a selector from Saga with select
effect, it passes the root state, so the example would be :
import Types from 'Types';
export const getReduxCounter = (state: Types.RootState) => state.counters.reduxCounter;
The problem with this is when you try to write a unit test for this selector :
const countersState: CountersState= {
reduxCounter: 0
};
const state: Types.RootState = {
counters: countersState,
};
it('getReduxCounter', () => {
expect(getReduxCounter(state)).toEqual(0);
});
But this way you have a warning because of missing keys in state
corresponding to the other reducers. Is there a way to avoid this message ?
Hey, IMO you shouldn't test your reducers using root reducer, but in isolation.
Your assertions would look like this:
it('getReduxCounter', () => {
expect(getReduxCounter({ countersState })).toEqual(0);
});
The problem is that selectors called from sagas with the select
saga effect expect the root state as parameter :(
That's why I passed root state as parameter, please look closely.
In your specific example it should be ({ counters }) but I expected you can figure that out.
At the risk of being told to take this to Stack Overflow, just want to point out a subtlety in @chawax's comments I've also been struggling with and I think was dropped/overlooked from the conversation here:
redux-saga
's select
effect will call your selector with the root state type of the store. Examples in this repo generally show selectors that work on the "child" or "sub" state. I like writing sub state selectors, so I've actually gone as far as creating root state selectors that wrap my sub state selectors just to get things to work with redux-saga
.
Is there a better way to do this or a pattern that works nicely with redux-saga
's select
? If so, is it something we can add an example for here in this great repo? If the answer already exists here, I'd say we should clarify it because I, and possibly others, haven't figured it out.
Hey @asutula, that's cool and thanks for clarifying :)
I think I misunderstood the problem initially because I have almost zero experience with sagas and I skimmed briefly through the example.
Now I think this should fix the @chawax problem:
const countersState: CountersState= {
reduxCounter: 0
};
const state = {
counters: countersState,
} as Types.RootState; // use an assertion to fix the error
it('getReduxCounter', () => {
expect(getReduxCounter(state)).toEqual(0);
});
To be honest I'm really suprised to hear such limitation for redux saga, unfortunately I cannot suggest any of above approaches. First one adds tight coupling with rootReducer and the other is adding extra indirection and boilerplate to the codebase. Maybe tight coupling is a bit lesser evil but still really unfortunate and annoying for testing.
If you can find some more optimal approach I would be happy to add it to the guide.
Thanks for the info @piotrwitek. That suggestion works if you want to write your selectors to operate on the root state type. I prefer to write my selectors to operate on sub state types because it keeps my code more modular and easy to test. The testing case is simple that way, and consistent with the examples here. Where it does cause some extra code is in the usage of the selectors in sagas. Here's an example of how I use one such selector (I mentioned above that I write a "wrapper" that takes in the root state and passed the correct sub state to the selector):
import { feedOffsetForGroup } from './selectors'
import { RootState } from '../../../Redux/Types'
function * handleLoadGroupItemsRequest(action: ActionType<typeof loadFeedItems.request>) {
const { id, limit } = action.payload
const offset = yield select((state: RootState, id: string) => feedOffsetForGroup(state.group.feed, id), id)
...
}
Thanks @piotrwitek . Your solution removes the warnings ! I also had to disable a TSLint rule in tslint.json
:
"no-object-literal-type-assertion": false,
@asutula I looked at saga selectors and in principle it's the same as regular redux state selectors, the only difference is an additional argument.
So, IMO you can still use modular selectors and the way you're using it in the provided example is the same as I'm using them in connected components here in this repo.
The only part I'm confused about is why you need arguments in selectors? I have written hundreds of selectors and never needed an argument. My best guess here is that when you need an argument you should basically use a selector factory function instead, which is a much nicer functional api.
const feedOffsetSelector = createFeedOffsetSelectorForGroup(id);
select((state: RootState) => feedOffsetSelector(state.group.feed))
Sorry for the slow reply here @piotrwitek. I spent some time thinking about your recommendation, I think that makes perfect sense. I agree that the more functional API resulting from using a factory function is good.
A little background on what led to this:
The reason I sometimes needed an argument in a selector (or now need to use a factory function), is because sometimes I need to query data out of the state by some value that doesn't exist in the state. I do realize there are many people that think that all the needed data should exist in the state tree, but when doing that, I've found myself making decisions about how to manage that state based on arbitrary things like UI animations... "oh I can't clear or change the currentlyViewedImageId
state until the image viewer dismissal animation is complete", for example. In cases like that, it's just easier to set an imageId
prop on the image viewer component, but of course, that prop then needs to be passed into a selector/factory function.
@asutula Great point, I totally agree with what you say about arguments selectors and that sometimes you need to query data by arg that is not in the state and it shouldn't be there just for the sake of selector. 👍
Btw. I would be interested to integrate redux-saga
setup to the playground project with a single saga example that can recreate this redux-observable
example from the docs: https://github.com/piotrwitek/react-redux-typescript-guide#redux-observable.
We could then inject it to the docs.
Let me know if you could help with that so I'll reopen this issue or create a new one.