Dojo : TDD Against the Time with React and Redux
This project is maintained by Bogala
Redux reducers handle state transitions, but they must be handled synchronously.
But what about Async events like User interactions, ajax calls (with cancellation), web sockets or animations?
See more in my presentation async-reduc-observable
What is an observable ?
What is RxJS ? It contains Observables and functions to create and compose Observables, also known as “Lodash for async”. RxJS combines the Observer and Iterator patterns, functional programming and collections in an ideal way to manage sequences of events.
of('hello')
from ([1, 2, 3, 4])
interval(1000)
ajax('http://example.com')
webSocket('ws://echo.websocket.com')
myObservable.subscribe(
value => console.log('next', value),
err => console.roor('error', err),
() => console.info('complete!')
);
An Epic is the core primitive of redux-observable.
It is a function that takes a stream of all actions dispatched and returns a stream of new actions to dispatch.
To prepare next step, we have to change the PLAY action :
actions.ts
export const PLAY = 'PLAY';
export const PLAYED = 'PLAYED';
export const PAUSED = 'PAUSED';
Now, in the reducer, PLAY becomes PLAYED. (PLAY will be used for the loop)
export default (state: MainState = initialState, action: Action) => {
switch (action.type) {
case PLAYED: {
const finalState = play(state);
return { ...finalState };
}
default:
return state;
}
};
First, we have to install packages:
yarn add rxjs redux-observable
Types are included in each package. We don’t have to add any @types/rxjs
or @types/redux-observable
Let’s code our first test: epic.spec.ts
configure({ adapter: new Adapter() });
jest.useFakeTimers();
const epicMiddleware = createEpicMiddleware(epic);
const mockStore = configureStore([epicMiddleware]);
let store: MockStore<{}>;
describe('Epic', () => {
beforeEach(() => {
store = mockStore({});
});
afterEach(() => {
epicMiddleware.replaceEpic(epic);
});
test('Dispatch played when launched', () => {
store.dispatch({ type: PLAY });
jest.runOnlyPendingTimers();
expect(store.getActions()).toEqual([
{ type: PLAY },
{ type: PLAYED }
]);
});
});
That makes an epic like this:
export default (action$: ActionsObservable<Action>) =>
action$.ofType(PLAY)
.switchMap(() =>
Observable.interval(50)
.mapTo({ type: PLAYED })
);
switchMap ?
A
switchMap
is a comination of a switchAll and a map.
switchAll
subscribes and produces values only from the most recent inner sequence ignoring previous streams.In the diagram below you can see the
H
higher-order stream that produces two inner streamsA
andB
. TheswitchAll
operator takes values from the A stream first and then from the streamB
and passes them through the resulting sequence.Here is the code example that demonstrates the setup shown by the above diagram:
const a = stream(‘a’, 200, 3); const b = stream(‘b’, 200, 3); const h = interval(100).pipe(take(2), map(i => [a, b][i])); h.pipe(switchAll()).subscribe(fullObserver(‘switchAll’));
If we use directly switchMap :
const a = stream(‘a’, 200, 3); const b = stream(‘b’, 200, 3); const h = interval(100).pipe(take(2), switchMap(i => [a, b][i])); h.subscribe(fullObserver(‘switchAll’));
for more informations, please refer to the Max NgWizard K’s post on medium
A second test:
test('Dispatch cancelled when paused', () => {
store.dispatch({ type: PLAY });
jest.runOnlyPendingTimers();
store.dispatch({ type: PAUSED });
jest.runOnlyPendingTimers();
expect(store.getActions()).toEqual([
{ type: PLAY },
{ type: PLAYED },
{ type: PLAYED },
{ type: PAUSED }
]);
});
Our new epic:
export default (action$: ActionsObservable<Action>) =>
action$.ofType(PLAY)
.switchMap(() =>
Observable.interval(50)
.takeUntil(action$.ofType(PAUSED))
.mapTo({ type: PLAYED })
);
We have to change our store index.ts:
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import reducer from './reducer';
import { compose } from 'recompose';
import epic from './epic';
// tslint:disable-next-line:no-any
const composeEnhancers =
// tslint:disable-next-line:no-any
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const epicMiddleware = createEpicMiddleware(epic, {
dependencies: { }
});
export const configureStore = () => (
createStore(
reducer,
composeEnhancers(applyMiddleware(epicMiddleware))
)
);
export {MainState, Ant} from './reducer';
Now, we are using compose to enhance previous configuration with epicMiddleWare (redux-observable).
Our mapDispatchToProps doesn’t need interval anymore. we will use dispatch to call redux an epic middleware :
const mapDispatchToProps: MapDispatchToProps<AppEventProps, AppProps> = (dispatch, ownProps) => ({
onPlay: () => {
dispatch({ type: PLAY } as Action);
},
onPause: () => {
dispatch({ type: PAUSED } as Action);
}
});
But with this our test won’t work, we make one too many dispatch call when the button is clicked:
test('Pause button stop dispatchs', async () => {
// tslint:disable-next-line:no-any
(store.dispatch as any).mockClear();
const wrapper =
mount(<Provider store={store}><MemoryRouter initialEntries={['/']}><App /></MemoryRouter></Provider>);
await wrapper.find(AvPlayArrow).simulate('click');
jest.runOnlyPendingTimers();
expect(store.dispatch).toHaveBeenCalled();
await wrapper.find(AvPause).simulate('click');
jest.runOnlyPendingTimers();
expect(store.dispatch).toHaveBeenCalledTimes(2);
});
test('stop stopped should not make exception', async () => {
// tslint:disable-next-line:no-any
(store.dispatch as any).mockClear();
const wrapper =
mount(<Provider store={store}><MemoryRouter initialEntries={['/']}><App /></MemoryRouter></Provider>);
await wrapper.find(AvPause).simulate('click');
jest.runOnlyPendingTimers();
expect(store.dispatch).toHaveBeenCalledTimes(1);
});
If you want to begin here, you can download this sources
Please try over 900 movements… Your ant needs a bigger grid. So, now we need a dynamic size for our grid.
If your Ant is on the border of the grid:
When you’re done, you can go to the next step : Advanced Typescript
5 Steps to reproduce every cycle:
Before each test, launch a five minutes timer.
All of your code must be covered by unit tests.
We’ll avoid any
as much as possible (implicit or not).