Skip to content

Instantly share code, notes, and snippets.

@evertbouw
Last active May 17, 2019 13:59
Show Gist options
  • Save evertbouw/94d4a9fdce93b34c31614a673acab73f to your computer and use it in GitHub Desktop.
Save evertbouw/94d4a9fdce93b34c31614a673acab73f to your computer and use it in GitHub Desktop.
Writing tests for epics with state$ and using marble diagrams. More information on marble testing can be found at https://github.com/ReactiveX/rxjs/blob/master/doc/marble-testing.md
import { Action } from "redux";
import { ActionsObservable, Epic, StateObservable } from "redux-observable";
import { TestScheduler } from "rxjs/testing";
const assertDeepEquals = (actual: any, expected: any) => {
expect(actual).toEqual(expected);
};
export const marbleTest = <T extends Action, O extends T = T, S = void, D = any>({
epic,
actions,
states = "",
expected,
values,
dependencies,
}: {
epic: Epic<T, O, S, D>;
actions: string;
states?: string;
expected: string;
values: { [marble: string]: T | O | S };
dependencies?: D;
}) => {
const testScheduler = new TestScheduler(assertDeepEquals);
testScheduler.run(({ hot, expectObservable }) => {
const state$ = new StateObservable<S>(hot<S>(states, <{ [marble: string]: S }>values), <S>values.s);
const action$ = new ActionsObservable<T>(hot<T>(actions, <{ [marble: string]: T }>values));
const output$ = epic(action$, state$, <D>dependencies);
expectObservable(output$).toBe(expected, values);
});
};
import { marbleTest } from "./epicMarbleTest";
import { rootReducer, pingEpic, ping, pong, setMessage } from "./reducer";
test("ping epic", () => {
const s = rootReducer(undefined, { type: "" });
const t = rootReducer(undefined, setMessage("world"));
marbleTest({
epic: pingEpic,
actions: "a -- a",
states: " -- t",
expected: "5s 1 -- 2",
values: {
a: ping(),
s, // s is used as the initial state, doesn't need to go in the marble
t,
1: pong(s.message),
2: pong(t.message)
}
});
});
import { combineReducers, Reducer } from "redux";
import { Epic, ofType } from "redux-observable";
import { mergeMap, take, map, delay } from "rxjs/operators";
export const PING: "PING" = "PING";
export const PONG: "PONG" = "PONG";
export const SET_MESSAGE: "SET_MESSAGE" = "SET_MESSAGE";
export const ping = () => ({
type: PING
});
export const pong = (message: string) => ({
type: PONG,
payload: message
});
export const setMessage = (message: string) => ({
type: SET_MESSAGE,
payload: message
});
export type AllActions = ReturnType<typeof ping | typeof pong | typeof setMessage>;
export type MyReducer<S> = Reducer<S, AllActions>;
export const messageReducer: MyReducer<string> = (state = "Hello", action) => {
if (action.type === SET_MESSAGE) {
return action.payload;
}
return state;
};
export const rootReducer = combineReducers({
message: messageReducer
});
export type MyState = ReturnType<typeof rootReducer>;
export type MyEpic<O extends AllActions> = Epic<AllActions, O, MyState>;
export const pingEpic: MyEpic<ReturnType<typeof pong>> = (action$, state$) =>
action$.pipe(
ofType(PING),
mergeMap(() =>
state$.pipe(
take(1),
map(state => state.message),
delay(5000),
map(pong)
)
)
);
@robbiec-medopad
Copy link

robbiec-medopad commented Dec 12, 2018

What versions of the libraries are you using? This doesn't type check for me with

redux@4.0.1
redux-observable@1.0.0
rxjs@6.3.3
typescript@3.2.1

Edit: fixed this by adding

interface Values<T> {
  [marble: string]: T;
}

then change the following lines

    const state$ = new StateObservable(hot<S>(states, values as Values<S>), values.s);
    const action$ = new ActionsObservable(hot<T>(actions, values as Values<T>));

@evertbouw
Copy link
Author

This was for typescript 2.8 I think. In my project I added a typecast like you did, updated the example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment