typesafe-actions
typesafe-actions copied to clipboard
I would like an explanation of the Service type in epics.
In the repo you write:
PS: If you're wondering what the Services type is in the epics signature and how to declare it in your application to easily inject statically typed API clients to your epics also ensuring for easy mocking while testing resulting in clean architecture, please create an issue for it and perhaps I'll find some time in the future to write an article about it.
I would like to read such an article :)
IssueHunt Summary
Sponsors (Total: $40.00)
-
issuehunt ($40.00)
Become a sponsor now!
Or submit a pull request to get the deposits!
Tips
- Checkout the Issuehunt explorer to discover more funded issues.
- Need some help from other developers? Add your repositories on IssueHunt to raise funds.
If you initialize your store like this :
const dependencies = {
provider1 = new Provider1(),
provider2 = new Provider2(),
provider3 = new Provider3()
};
function configureStore() {
const epicMiddleware = createEpicMiddleware<RootAction, RootAction, RootState>({
dependencies
});
const middlewares = [epicMiddleware];
const enhancer = composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(rootReducer, enhancer);
epicMiddleware.run(rootEpic);
return store;
}
const store = configureStore();
export default store;
Then the type of Services
would be :
export type RootServices = typeof dependencies;
Which is equivalent to :
export type RootServices = {
provider1: Provider1,
provider2: new Provider2,
provider3: new Provider3
}
And then you woul write your epics like this :
export const exampleFlow: Epic<RootAction, RootAction, RootState, RootServices> =
(action$, state$, { provider1 }) =>
action$.pipe(
filter(isActionOf(exampleAsyncAction.request)),
concatMap(action =>
from(provider1 .exampleFunction()).pipe(
map(exampleAsyncAction.success),
catchError(() => of(exampleAsyncAction.failure()))
)
)
);
One caveat of this approach is it make painfull to test your epics with Jest since you would have to inject all dependencies (or mock of them) in all your epics.
I personnaly prefer to avoid a global RootServices
type and write more explicitly the exact providers I need for each Epic.
For the above exemple it would give :
export const exampleFlow: Epic<RootAction, RootAction, RootState, { provider1: Provider1 }> =
(action$, state$, { provider1 }) =>
action$.pipe(
filter(isActionOf(exampleAsyncAction.request)),
concatMap(action =>
from(provider1 .exampleFunction()).pipe(
map(exampleAsyncAction.success),
catchError(() => of(exampleAsyncAction.failure()))
)
)
);
It makes your epics it little bit more verbose but it much more easy to write tests.
@TomDemulierChevret You mentioned a caveat with mocking of dependencies, so could you please explain how your second example is making it more easy to write tests?
Personally, I cannot see any benefits in your example except more work because of importing and declaring that extra explicit type annotations that you need in each epic.
For exemple if I want to write tests for the epics of a particular subState that only requires 1 provider and not all of them.
jest.mock('../../providers/provider1');
const provider1: jest.Mocked<Provider1> = new Provider1() as any;
const providers = {
provider1
};
let state$: StateObservable<RootState>;
describe('sub-state epics', () => {
beforeEach(() => {
state$ = new StateObservable<RootState>(new Subject<RootState>(), {
subState: initialSubState
} as RootState);
});
describe('firstSubStateEpic', () => {
beforeEach(() => {
provider1.function1.mockClear();
});
test('test1', done => {
provider1.function1.mockImplementation(() => Promise.resolve(data));
const action$ = ActionsObservable.of(someAsyncAction.request(defaultData));
firstSubStateEpic(action$, state$, providers).subscribe((outputAction: Action) => {
expect(provider1.function1).toBeCalled();
expect(outputAction).toEqual(someAsyncAction.success(data));
done();
});
});
});
});
This way when I add new providers or modify some (outside of provider1
) my test will still have valid typing and will still run perfectly.
If I pass RootServices
to my epics I would have to mock in some way every provider (even just a dummy mock).
But again it's just a matter of personnal taste.
This way when I add new providers or modify some (outside of
provider1
) my test will still have valid typing and will still run perfectly.
With RootServices
it's no different, you always have all the typings, because it's implementation driven, when you implement a new service type will updated itself automatically.
If I pass
RootServices
to my epics I would have to mock in some way every provider (even just a dummy mock).
RootServices
is just a type, it has no impact on runtime so you don't have to mock anything extra except what you're testing. It would work perfectly fine with your example mocking only one provider that is used.
Perhaps your problem is related to RootServices
declaration, which is done not in the ambient context but in runtime and that's why you are dragging runtime code with import statement requiring to mock every dependency.
I'm not talking about runtime issue but about typings.
How would you declare RootServices
?
Because when I tried this approach, my const providers
had to have every provider declared (event with just dummy mocking) for the typings to work.
I suggest a different approach from yours.
I have just added the approach that I'm recommending in my react-redux-typescript-guide
repo:
- https://github.com/piotrwitek/react-redux-typescript-guide#testing-epics
Main point is that I'm using a simple typesafe providers mock object which I can easily reuse in every test, and also I don't need to mock anything else comparing to what you're doing with jest.mock('../../providers/provider1'); const provider1: jest.Mocked<Provider1> = new Provider1() as any;
which is IMO unnecessary.
Compare it to this which can handle all tests you'll ever write:
// Simple typesafe mock of all the services, you dont't need to mock anything else
// It is decoupled and reusable for all your tests, just put it in a separate file
const services = {
logger: {
log: jest.fn<Services['logger']['log']>(),
},
localStorage: {
loadState: jest.fn<Services['localStorage']['loadState']>(),
saveState: jest.fn<Services['localStorage']['saveState']>(),
},
};
Interesting.
I was using jest.mock
because my providers are ES6 class but your approach would work too.
But wouldn't your approach force you to modify the const services
everytime you modify yours providers (or add another one) ?
Yes it would require to update the mock, but it's just a simple JS object encapsulated in one place, so it's really easy and fast to do. Moreover, you have a complete type-safety so you'll know when you need to do it and you'll use refactoring to update all of your tests at once because of shared mock.
In contrast, when automocking each provider separately with jest.mock
it's hard to tell how that would work with type-safety and if refactoring would work across the board as nicely in all tests files.
All valid points, I will try it.
I've tried your way and it works pretty well.
One issue thought : if your providers are classes with private methods/members, mocking them the way you did won't be type compliant.
So you'll end having to do something like this (only public methods are mocked) :
const services = {
logger: ({
log: jest.fn<Services['logger']['log']>(),
} as Partial<Logger>) as jest.Mocked<Logger>,
localStorage: ({
loadState: jest.fn<Services['localStorage']['loadState']>(),
saveState: jest.fn<Services['localStorage']['saveState']>(),
} as Partial<LocalStorage>) as jest.Mocked<LocalStorage>,
};
If you have a better way, I'll gladly hear it.
I'm not using classes so I cannot tell, but please check these manual mocking approaches from the docs, I'm pretty sure one of them should be working good with classes: https://jestjs.io/docs/en/es6-class-mocks#manual-mock
@issuehunt has funded $40.00 to this issue.
- Submit pull request via IssueHunt to receive this reward.
- Want to contribute? Chip in to this issue via IssueHunt.
- Checkout the IssueHunt Issue Explorer to see more funded issues.
- Need help from developers? Add your repository on IssueHunt to raise funds.
Hi, @piotrwitek could you please add a fake async function which actually returns something to test in https://github.com/piotrwitek/react-redux-typescript-guide#testing-epics? There are some warnings emerged when I did that.
@ithinco could you please provide more details about what you need exactly please? I don't want to guess.