Mock local variable derived from redux store redux saga testing - reactjs

I have written a saga that uses a yield select to get part of the redux store state and store it in a local variable. The variable is an object with three different keys and each key's value is an array of objects within that.
My redux-saga tests keep failing because I create this local variable within the saga that uses data from the initial yield select and in the tests that variable is always undefined, which causes the rest of my tests in that saga to fail. I've seen a lot of examples about how to mock the return state of yield select in a redux saga, but it's always in the context of the next redux-saga call. How do I mock the return state for a local variable?
Here is my code:
export default function* orderSelectionFlow({ payload }) {
try {
const orders = yield select(getOrders); // retrieve relevant part of redux store
const activeOrder = orders[payload.orderStatus].find(order => order.id === payload.orderId); // this variable is always undefined in my tests, because I am not sure how to mock `orders` for a local variable.
const activeOrderId = payload.orderId;
const isConnected = yield select(getIsConnected); // test for this select fails because activeOrder is undefined
My test ( up to the relevant point) is
describe('test order selection flow', () => {
const navSpy = jest.spyOn(AppNavigator, 'navigate');
const action = {
type: 'galactica/orders/VIEW',
payload: {
orderStatus: 'OPEN',
orderId: 1
}
};
afterAll(() => {
jest.resetModules();
navSpy.mockRestore();
});
it.next = sagaHelper(orderSelectionFlow(action));
it.next('should get all orders', (result) => {
expect(result).toEqual(select(getOrders));
});
it.next('should use connected state', (result) => {
expect(result).toEqual(select(getIsConnected));
});
Orders is currently undefined, but if I could mock the value it would be
orders: {
'OPEN': {
orderId: 1
}
}

I would try using redux-saga-tester: https://github.com/wix/redux-saga-tester . It let's you specifiy the initial state in the options. I'm not quite sure what all your code looks like, but I think you could do something like below.
describe('test order selection flow', () => {
const navSpy = jest.spyOn(AppNavigator, 'navigate');
const action = {
type: 'galactica/orders/VIEW',
payload: [{
orderStatus: 'OPEN',
orderId: 1
}]
};
afterAll(() => {
jest.resetModules();
navSpy.mockRestore();
});
it('should get all orders', (result) => {
const tester = new SagaTester({
initialState: {
orders: {
'OPEN': {
orderId: 1
}
}
}
});
tester.start(saga);
tester.dispatch(action);
// ... do your expects here
// You may need to use `tester.waitFor` to wait for an action to be dispatched
// If you want to expect a certain action was called, use `getCalledActions`
});

Related

testing issue in redux saga method using jest

I am testing my redux saga method using jest. I am facing some issues. Attaching the saga method, test case & the console response.
Let me know where I missed it. Tried lots of googling. But couldn’t find any solution.
Any help would be appreciated.
thanks!
/* saga method starts*/
export function* fetchStripeLocations() {
console.log('saga fetchStripeLocations called:');
try {
const BE = yield select((state) => state.BE);
const PaymentRequestFormReducer = yield select(state => state.PaymentRequestFormReducer.toJS());
const { stripeLocationsMap } = PaymentRequestFormReducer;
const paymentsEnabled = isPaymentsEnabled(BE);
if (!stripeLocationsMap.data && !stripeLocationsMap.isLoading && paymentsEnabled) {
yield put(actionCreator.fetchingStripeLocations());
const url = actionCreator.URL.FETCH_STRIPE_LOCATIONS;
const response = yield call(beAPIResource.get, url, {}, { topLoader: false, isPrimaryAPI: false, cancellable: false });
const { qcrEnabled, stripeLocationsMap } = response.data;
yield put(actionCreator.setStripeLocations({ qcrEnabled, stripeLocationsMap }));
}
} catch (e) {
yield put(actionCreator.setStripeLocations({ stripeLocationsMap: {} }));
yield put(dataFetchingError(e, "Something went wrong please try again later"));
}
}
/* saga method ends*/
/* test case starts*/
import { testData } from "./TestReducerData";
import { commonTestData } from "../../../../../../__test__/commonTestData";
test('fetchStripeLocations testing should pass', async () => {
const dispatchedActions = [];
const stripeDummyData = iMap({
isLoading: false,
data: null,
qcrEnabled: false
});
const mockData = {
qcrEnabled : false,
stripeLocationsMap: {
isLoading: false,
data: null,
qcrEnabled: false
}
};
const mRequest = jest.fn(() => Promise.resolve(mockData));
const mockState = {
BE: commonTestData,
PaymentRequestFormReducer: testData.initialState
};
await runSaga({
dispatch: (action) => dispatchedActions.push(action),
getState: () => mockState,
}, PaymentRequestFormSaga.fetchStripeLocations).done;
console.log('dispatchedActions', dispatchedActions);
console.log('mRequest.mock:', mRequest.mock);
expect(mRequest.mock.calls.length).toBe(1);
expect(dispatchedActions).toEqual([ActionCreator.setStripeLocations(mockData)]);
});
/* test case ends*/
toBe is like a reference check, but of course these two objects are not the same instance. toBe is rarely the right choice for objects
Use .toBe to compare primitive values or to check referential identity
of object instances. It calls Object.is to compare values, which is
even better for testing than === strict equality operator.
You can add jest-immutable-matchers to your project to compare immutable-js objects for value equality:
import * as matchers from 'jest-immutable-matchers';
describe('My suite', function () {
beforeEach(function () {
expect.extend(matchers);
});
it('passes if the immutable objects are equal', function () {
expect(Immutable.Map({a: 1})).toEqualImmutable(Immutable.Map({a: 1}));
});
});
If you want to test equality of two immutable objects yourself, use Immutable.is,
e.g. Immutable.is(Immutable.Map({a: 1}), Immutable.Map({a: 1}))

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

Redux saga testing using runSaga not updating the state

So I am trying to test out a redux saga using the runSaga function. This saga gives to call to an API and stores its response in the state by calling an action. After storing it, I am retrieving the stored data using a selector and performing some actions. I am able to mock the API response and also call the success action once the API has returned.
When in the next line when I try to call the selector to fetch the data from the store, it seems to be getting the initial state from the store and not the updated one after the API success.
Here is my code:
// loadSaga.js. This is the saga that I'm testing
export function* loadSaga(request) {
const endpoint = yield select(ListEndpointSelector);
const listResponse = yield call(request, endpoint, 'GET', {}, throwIfNot404);
console.log(listResponse); // Returns the correct mocked API response
yield put(loadListSuccess(listResponse));
const { list as newList } = yield select(listSelector);
console.log(newList); // Returns empty array
// ... do something with newList but newList is empty
};
// loadSaga.test.js
import { runSaga } from 'redux-saga';
import * as reduxSagaEffects from 'redux-saga/effects';
const mockState = {
user: {
list: [],
},
},
};
describe('loadSaga', () => {
it('returns the correct list of users', async () => {
const dispatchedActions = [];
const mockUsers = [
{
id: 1,
name: 'abc',
},
{
id: 2,
name: 'xyz',
},
]
const mockfn = jest.fn().mockImplementationOnce(() => mockUsers);
// eslint-disable-next-line
const m = jest.mock('redux-saga/effects', () => ({
...reduxSagaEffects,
call: mockfn,
}));
const { loadSaga } = require('./loadSaga.js');
runSaga({
dispatch: (action) => dispatchedActions.push(action),
getState: () => (mockState),
}, loadSaga);
console.log(dispatchedActions);
});
When I console listResponse, I get the correct mockedResponse from the API which I set in the mockFn. But when I console the newList, it returns [] which seems to be the list set in the mockState.
The console.log for dispatched actions shows the correct actions being dispatched, even the loadListSuccess with the mocked API response being passed to it.
Since the yield select(listSelector) is not returning the correct output, I am not able to test the further test cases. What should I change so that I am able to retrieve the current updated state in the selector?
loadSaga saga does not connect to the redux store. select(selector, ...args) just creates an effect object. When the redux store uses redux-saga as middleware, redux-saga can only get the getState method of redux and pass it to the effect created by select.
You can use getState() option of runSaga to create a mocked store. You have to maintain the correctness of the state data yourself to ensure that the subsequent logic that depends on the list is executed correctly.
Besides, you don't need to mock the call effect creator of redux-saga, since loadSaga accepts a request handler, you can create a mock for it and pass it to the third parameter of runSaga.
E.g.
loadSaga.ts:
import { select, call, put } from 'redux-saga/effects';
const listSelector = (state) => {
console.log('state: ', state);
return state.user;
};
export function loadListSuccess(payload) {
return { type: 'LOAD_LIST_SUCCESS', payload };
}
export function* loadSaga(request) {
const listResponse = yield call(request);
console.log('listResponse: ', listResponse);
yield put(loadListSuccess(listResponse));
const { list } = yield select(listSelector);
console.log('list: ', list);
}
loadSaga.test.ts:
import { runSaga } from 'redux-saga';
import { loadListSuccess, loadSaga } from './loadSaga';
describe('68632358', () => {
test('should pass', async () => {
const dispatchedActions: any[] = [];
const mockUsers = [
{ id: 1, name: 'abc' },
{ id: 2, name: 'xyz' },
];
const mockState = {
user: {
list: mockUsers,
},
};
const mRequest = jest.fn().mockResolvedValue(mockUsers);
await runSaga(
{
dispatch: (action) => dispatchedActions.push(action),
getState: () => mockState,
},
loadSaga as any,
mRequest,
).toPromise();
expect(mRequest).toBeCalled();
expect(dispatchedActions).toEqual([loadListSuccess(mockUsers)]);
});
});
test result:
PASS src/stackoverflow/68632358/loadSaga.test.ts
68632358
✓ should pass (21 ms)
console.log
listResponse: [ { id: 1, name: 'abc' }, { id: 2, name: 'xyz' } ]
at src/stackoverflow/68632358/loadSaga.ts:14:11
console.log
state: { user: { list: [ [Object], [Object] ] } }
at listSelector (src/stackoverflow/68632358/loadSaga.ts:4:11)
console.log
list: [ { id: 1, name: 'abc' }, { id: 2, name: 'xyz' } ]
at src/stackoverflow/68632358/loadSaga.ts:17:11
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.641 s, estimated 2 s
package version: "redux-saga": "^1.1.3"

Call redux action within redux-saga inside websocket callback (stomp + sockjs)

I am using redux and redux-saga in my project. Right now using WebSocket I have a problem calling a FETCH_SUCCESS redux action inside a callback of socket response. I tried making the callback a generator as well but didn't work as well.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
});
...
....
}
);
}
Or maybe this WebSocket should be implemented in a completely different way in redux-saga?
You won't be able to use yield put inside a callback function. Stompjs knows nothing about sagas, so it doesn't know what it's supposed to do when given a generator function.
The simplest approach, though not necessarily the best, is to go directly to the redux store in the callback, and dispatch the action without involving redux-saga. For example:
import store from 'wherever you setup your store'
// ...
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
store.dispatch({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
});
If you'd like to use a more redux-saga-y approach, I would recommend wrapping the subscription in an event channel. Event channels take a callback-based API and turn it into something that you can interact with using redux-saga's effects such as take
Here's how you might create the event channel:
import { eventChannel } from 'redux-saga';
function createChannel(token) {
return eventChannel(emitter => {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
emitter(response); // This is the value which will be made available to your saga
});
}
);
// Returning a cleanup function, to be called if the saga completes or is cancelled
return () => stomp.disconnect();
});
}
And then you'd use it like this:
function* websocketSaga() {
const token = yield select(selectToken);
const channel = createChannel(token);
while (true) {
const response = yield take(channel);
yield put({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
}
}
Promise should be the perfect fit. Just wrap the callback related code in a promise and resolve it in the callback function. After that use the yield to get the data from the promise. I have modified your code with the Promise below.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
const p = new Promise((resolve, reject) => {
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
resolve(response); // here resolve the promise, or reject if any error
});
...
....
}
);
});
try {
const response = yield p; // here you will get the resolved data
yield put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
} catch (ex) {
// handle error here, with rejected value
}
}
I will give you another way of managing this: create a component connected to redux where you will handle the WS subscription. This component will not render anything to the UI but will be useful for handling redux store interactions.
The main idea is, don't put everything into redux-saga, try and split it into multiple parts to make it easier to maintain.
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
function WSConnection(props) {
const {token, fetchDone} = props;
const [stomp, setStomp] = React.useState();
const onMessage = React.useCallback(message => {
const response = JSON.parse(message.body);
fetchDone(response.dataResponse);
}, [fetchDone]);
const onConnect = React.useCallback(frame => {
const subscription = stomp.subscribe('/queue/data', onMessage);
// cleanup subscription
return () => subscription.unsubscribe();
}, [stomp, onMessage]);
const onError = React.useCallback(error => {
// some error happened, handle it here
}, []);
React.useEffect(() => {
const header = {Authorization: `Bearer ${token}`};
stomp.connect(header, onConnect, onError);
// cleanup function
return () => stomp.disconnect();
}, [stomp])
React.useEffect(() => {
setStomp(Stomp.over(socket));
}, []);
return null;
}
const mapStateToProps = state => ({
... // whatever you need from redux store
});
const mapDispatchToProps = dispatch => ({
... // whatever actions you need to dispatch
});
export default connect(mapStateToProps, mapDispatchToProps)(WSConnection);
You can also take it a step further and extract the stomp logic into another file and reuse it wherever you will need it.
It's not wrong to put everything into redux-saga but it's a nice alternative to handle WS connections inside components connected to redux (and easier to understand to people who are not completely familiar with redux-saga and channels etc).
I have the same stack over the years and only recently I faced websockets over Stomp client.
None of the above solutions doesn't work for me both technically and mentally
Reasons:
I don't like channels with Stomp because the only way to manipulate connections in more surgical way you have to use global state object (for me - it's redux). It doesn't seems right even if you storing only random generated IDS (with unsubscribe function it will be... read more here about store serialization
the way with container another pain in the ... (you know where). Again redux and a lot of under-the-hood functionality used without any reason
another way with promises: again without storing helpful connection info and some DI by using promises inside generators. This narrows down the implementation choice
So:
I need to have connection info (I decided to use state but not in: redux, component state. Singleton state). Stomp doesn't force you to place ID but I do because I want to manage connections by myself
I need one entry point without: promises, iterators and a lot of things that will be pain for future-me. One place to "rule them all" (as I want)
- activate: login
- deactivate: logout
- subscribe: componentDidMount
- unsubscribe: componentWillUnmount
DI by request in one place (passing store.dispatch to constructor only if need it) // main topic of the question
And I wrote this implementation that perfectly works for me:
import SockJS from 'sockjs-client';
import {
Client,
IMessage,
messageCallbackType,
StompHeaders,
} from '#stomp/stompjs';
import { Action, Dispatch } from 'redux';
type ConnectionId = string;
interface IServiceConfig {
url: string;
dispatch?: Dispatch;
}
export default class Stomp {
serviceConfig: IServiceConfig = {
dispatch: null,
url: null,
};
ids: ConnectionId[] = [];
stomp: Client;
constructor(config: IServiceConfig) {
this.serviceConfig = { ...config };
this.stomp = new Client();
this.stomp.webSocketFactory = () => {
return (new SockJS(config.url));
};
}
alreadyInQueue = (id: ConnectionId): boolean => {
return Boolean(this.ids.find(_id => id === _id));
};
subscribeByDispatchAction = (
destination: string,
callback: (message: IMessage) => Action,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => {
this.serviceConfig.dispatch(callback(message));
},
headers,
);
this.ids.push(headers.id);
return;
}
console.warn(`Already in queue #${headers.id}`);
};
subscribe = (
destination: string,
callback: messageCallbackType,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => callback(message),
headers,
);
this.ids.push(headers.id);
this.logState('subscribe');
return;
}
console.warn(`Failed to subscribe over Socks by #${headers.id}`);
};
unsubscribe = (id: ConnectionId, headers?: StompHeaders): void => {
this.stomp.unsubscribe(id, headers);
this.ids.splice(this.ids.indexOf(id), 1);
};
activate = (): void => {
this.stomp.activate();
};
deactivate = (): void => {
if (this.ids.length === 0) {
this.stomp.deactivate();
return;
}
for (let i = 0; i < this.ids.length; i++) {
this.unsubscribe(this.ids[i]);
}
/**
* it seems like it's overkil but
* for me it works only if i do all
* the things as you see below
* - stomp deactivation
* - closing webSockets manually by using native constant // sockjs-client
* - closing webSockets instance by using returned value fron factory
*/
this.stomp.deactivate();
this.stomp.webSocket.close(
this.stomp.webSocket.CLOSED,
);
this.stomp.webSocketFactory().close();
};
getAllIds = (): readonly ConnectionId[] => {
return this.ids;
};
// debug method
logState = (method: string): void => {
/* eslint-disable */
console.group(`Stomp.${method}`);
console.log('this', this);
console.log('this.ids', this.getAllIds());
console.log('this.stomp', this.stomp);
console.groupEnd();
/* eslint-enable */
};
}
My configuration file
import { store } from '~/index';
import Stomp from '~/modules/_Core/services/Stomp';
import appConfig from '~/modules/Common/services/appConfig';
export const StompService = new Stomp({
dispatch: store?.dispatch,
url: `${appConfig.apiV1}/websocket`,
});
I hope that it will help someone

Testing Observable epic which invokes other epic

I am trying to test a Redux Observable epic which dispatches an action to invoke an other epic. Somehow the invoked epic is not called.
Lets say my epics looks like this;
const getJwtEpic = (action$, store, { api }) =>
action$.ofType('GET_JWT_REQUEST')
.switchMap(() => api.getJWT()
.map(response => {
if (response.errorCode > 0) {
return {
type: 'GET_JWT_FAILURE',
error: { code: response.errorCode, message: response.errorMessage },
};
}
return {
type: 'GET_JWT_SUCCESS',
idToken: response.id_token,
};
})
);
const bootstrapEpic = (action$, store, { api }) =>
action$.ofType('BOOTSTRAP')
.switchMap(() =>
action$.filter(({ type }) => ['GET_JWT_SUCCESS', 'GET_JWT_FAILURE'].indexOf(type) !== -1)
.take(1)
.mergeMap(action => {
if (action.type === 'GET_JWT_FAILURE') {
return Observable.of({ type: 'BOOTSTRAP_FAILURE' });
}
return api.getProfileInfo()
.map(({ profile }) => ({
type: 'BOOTSTRAP_SUCCESS', profile,
}));
})
.startWith({ type: 'GET_JWT_REQUEST' })
);
When I try to test the bootstrapEpic in Jest with the following code;
const response = {};
const api = { getJWT: jest.fn() };
api.getJWT.mockReturnValue(Promise.resolve(response));
const action$ = ActionsObservable.of(actions.bootstrap());
const epic$ = epics.bootstrapEpic(action$, null, { api });
const result = await epic$.toArray().toPromise();
console.log(result);
The console.log call gives me the following output;
[ { type: 'GET_JWT_REQUEST' } ]
Somehow the getJwtEpic isn't called at all. I guess it has something to do with the action$ observable not dispatching the GET_JWT_REQUEST action but I can't figure out why. All help is so welcome!
Assuming actions.rehydrate() returns an action of type BOOTSTRAP and the gigya stuff is a typo,
getJwtEpic isn't called because you didn't call it yourself 🤡 When you test epics by manually calling them, then it's just a function which returns an Observable, without any knowledge of the middleware or anything else. The plumbing that connects getJwtEpic as part of the root epic, and provides it with (action$, store) is part of the middleware, which you're not using in your test.
This is the right approach, testing them in isolation, without redux/middleware. 👍 So you test each epic individually, by providing it actions and dependencies, then asserting on the actions it emits and the dependencies it calls.
You'll test the success path something like this:
const api = {
getProfileInfo: () => Observable.of({ profile: 'mock profile' })
};
const action$ = ActionsObservable.of(
actions.rehydrate(),
{ type: 'GET_JWT_SUCCESS', idToken: 'mock token' }
);
const epic$ = epics.bootstrapEpic(action$, null, { api });
const result = await epic$.toArray().toPromise();
expect(result).toEqual([
{ type: 'GET_JWT_REQUEST' },
{ type: 'BOOTSTRAP_SUCCESS', profile: 'mock profile' }
]);
Then you'll test the failure path in another test by doing the same thing except giving it GET_JWT_FAILURE instead of GET_JWT_SUCCESS. You can then test getJwtEpic separately as well.
btw, ofType accepts any number of types, so you can just do action$.ofType('GET_JWT_SUCCESS', 'GET_JWT_FAILURE')

Resources