Redux mockStore's expected action is only returning first element of list - reactjs

const middlewares = [ thunk ];
const mockStore = configureStore(middlewares);
const originals = {
Actions: {}
};
const mocks = {
Actions: {
addPasswordResetRequest: spy(() =>{
return [
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST},
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS}
];
})
}
}
//(have functions to mock actions)
...
it('should create an action to request a password reset', (done) => {
nock('http://localhost:8080/')
.post('/password-reset-requests')
.reply(200);
var email = "test#email.com";
const expectedActions= [
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST},
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS}
];
const store = mockStore({}, [
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST},
{type: REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS}
], done);
store.dispatch(Actions.addPasswordResetRequest(email));
unMockActions();
});
Response I get:
Error: Expected [ { type: 'REQUEST_ADD_PASSWORD_RESET_REQUEST' },
{type: 'REQUEST_ADD_PASSWORD_RESET_REQUEST_SUCCESS' } ] to equal
{type: 'REQUEST_ADD_PASSWORD_RESET_REQUEST' }
When I run this code, I only get the first action returned, not the second one. And it's not a list, it's only one action. I need to test the sequence of the actions, not whether one action is called.

I experienced this when trying to test a reducer that had more than one action type in it. With Mocha you can only test 1 action at a time. Ive not tested any 'thunked' action creators yet, but i would expect that the mock action creator would test for the second action and that should be captured in a test for that action creator.

Related

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"

How to test a redux-thunk action that contains multiple API requests and array transformations?

I have a redux-thunk action that contains multiple API-requests that take data fetched from one endpoint to fetch other relevant data from a different endpoint and I also have a couple of array transformations to merge some of the data together.
Although I'm not sure if this is the best practice, for now, it does what I need. However, it has been difficult to test as I'm not sure what the correct approach is to test it. I have scoured the internet and looked at many different variations of "thunk" tests but mine is failing with every approach so far.
I will really appreciate some guidance on how to test a thunk action such as mine or perhaps better practices in implementing what I have if it makes testing easier.
My thunk-Action...
export const fetchTopStreamsStartAsync = () => {
return async dispatch => {
try {
const headers = {
'Client-ID': process.env.CLIENT_ID
};
const url = 'https://api.twitch.tv/helix/streams?first=5';
const userUrl = 'https://api.twitch.tv/helix/users?';
let userIds = '';
dispatch(fetchTopStreamsStart());
const response = await axios.get(url, { headers });
const topStreams = response.data.data;
topStreams.forEach(stream => (userIds += `id=${stream.user_id}&`));
userIds = userIds.slice(0, -1);
const userResponse = await axios.get(userUrl + userIds, { headers });
const users = userResponse.data.data;
const completeStreams = topStreams.map(stream => {
stream.avatar = users.find(
user => user.id === stream.user_id
).profile_image_url;
return stream;
});
const mappedStreams = completeStreams.map(
({ thumbnail_url, ...rest }) => ({
...rest,
thumbnail: thumbnail_url.replace(/{width}x{height}/gi, '1280x720')
})
);
dispatch(fetchTopStreamsSuccess(mappedStreams));
} catch (error) {
dispatch(fetchTopStreamsFail(error.message));
}
};
};
One of the many test approaches that have failed...
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import axios from 'axios';
import moxios from 'moxios';
import {
fetchTopStreamsStart,
fetchTopStreamsSuccess,
fetchTopStreamsStartAsync
} from './streams.actions';
const mockStore = configureMockStore([thunk]);
describe('thunks', () => {
describe('fetchTopStreamsStartAsync', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: responsePayload
});
});
const store = mockStore();
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
This is the error i'm getting in the failed test for the received value...
+ "payload": "Cannot read property 'forEach' of undefined",
+ "type": "FETCH_TOP_STREAMS_FAIL",
UPDATE: As #mgarcia suggested i changed the format of my responsePayload from [{ id: 1 }, { id: 2 }, { id: 3 }] to { data: [{ id: 1 }, { id: 2 }, { id: 3 }] } and now I'm not getting the initial error but now I'm receiving the following error:
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
What I still don't understand is does the test have to replicate the exact structure of the multiple API calls or that just mocking one response is enough? I'm still trying to figure out the cause of the Async callback... error.
You are mocking the axios request through moxios, but it seems that you are not returning the data in the expected format.
In your action creator you read the response data as:
const topStreams = response.data.data;
const users = userResponse.data.data;
But you are mocking the response so that it returns:
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
Instead, it seems that you should be returning:
const responsePayload = { data: [{ id: 1 }, { id: 2 }, { id: 3 }] };
Aside from the mock response, your code presents some further problems. First, as you have noticed yourself, you are only mocking the first request. You should mock the second request as well returning the desired data. Second, in your assertion you are expecting to have the actions created in:
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
This will not be true, as you are processing the responsePayload in the action creator, so that the payload with which you are calling fetchTopStreamsSuccess in the action creator will be different from responsePayload.
Taking all this into account, your test code could look like:
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const streamsResponse = [
{ user_id: 1, thumbnail_url: 'thumbnail-1-{width}x{height}' },
{ user_id: 2, thumbnail_url: 'thumbnail-2-{width}x{height}' },
{ user_id: 3, thumbnail_url: 'thumbnail-3-{width}x{height}' }
];
const usersResponse = [
{ id: 1, profile_image_url: 'image-1' },
{ id: 2, profile_image_url: 'image-2' },
{ id: 3, profile_image_url: 'image-3' }
];
const store = mockStore();
// Mock the first request by URL.
moxios.stubRequest('https://api.twitch.tv/helix/streams?first=5', {
status: 200,
response: { data: streamsResponse }
});
// Mock the second request.
moxios.stubRequest('https://api.twitch.tv/helix/users?id=1&id=2&id=3', {
status: 200,
response: { data: usersResponse }
});
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
expect(store.getActions()).toEqual([
fetchTopStreamsStart(),
{
"type": "TOP_STREAMS_SUCCESS",
"payload": [
{ "avatar": "image-1", "thumbnail": "thumbnail-1-1280x720", "user_id": 1 },
{ "avatar": "image-2", "thumbnail": "thumbnail-2-1280x720", "user_id": 2 },
{ "avatar": "image-3", "thumbnail": "thumbnail-3-1280x720", "user_id": 3 },
]
}
]);
});
});
Note that I have made up the structure of the fetchTopStreamsSuccess action to have a type attribute equal to TOP_STREAMS_SUCCESS and to have an attribute payload with the completeStreams data. You will probably have to accommodate that to the real structure of the fetchTopStreamsSuccess action you are creating for the test to pass.

Instantiate a new Howler Object in Action Creators and dispatch actions on events

So I'm trying to build a React.js + Redux audio player.
I'm using the Howler.js Library to process audio from an API.
I'm dispatching actions from the player control buttons and from other component such as a track list component.
When the user clicks on a track in the lis, I'm dispatching a track object to the Player Reducer. I instantiate a new Howl Object here which works but there is some issues here :
Because Howler is fetching the data from an API, this is an async task and it's an anti pattern. I know that, but here I'm not triggering events from Howler so it works, but it's a bit buggy.
I can't use event triggers because it would be async and it's a bad pratice to dispatch actions from the Reducer.
What I would like :
Instanciate a new Howler Object in my Action Creator.
Use the event triggers, e.g : onload() or onloaderror(), to dispatch an other action to tell the player that Howler is ready to play a song or if there was an error.
The code :
Action Creator
export const PLAYER_INITIALIZE = 'PLAYER_INITIALIZE'
export const initialize = () => {
return {
type: PLAYER_INITIALIZE,
}
}
export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
export const setTrack = (trackId) => {
return {
type: PLAYER_SET_TRACK,
}
}
export const PLAYER_PLAY_TRACK = 'PLAYER_PLAY_TRACK'
export const playTrack = () => {
return {
type: PLAYER_PLAY_TRACK,
}
}
Reducer
function PlayerReducer(state = initialPlayerState, action) {
switch (action.type) {
case PlayerActions.PLAYER_SET_TRACK:
if (state.audioObj.state() != 'unloaded')
state.audioObj.unload();
return {
...state,
audioObj: new Howl({
src: API.API_STREAM_TRACK + action.trackId,
html5: true,
preload: true,
onload: () => {
console.log("Track loaded succesfully.");
},
onloaderror: (id, err) => {
console.error("Load Error : " + err);
}
}),
trackMetadata: action.trackMetadata
};
case PlayerActions.PLAYER_PLAY_TRACK:
state.audioObj.play();
return {
...state,
isPlaying: true,
};
[...]
My idea is to instanciate Howler like this :
new Howler({
[ ... some options ],
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS
});
},
onloaderror: () => {
dispatch({
type: PLAYER_LOAD_ERROR
});
}
});
How can I edit this code to create a new Howl Object correctly in the Action Creator and pass it to the player on each track that I want to listen?
Can you give me an example ?
Thanks.
You can add redux-thunk to your app and then be able to dispatch multiple actions from a single action creator. After adding redux-thunk, your action could look like the following:
export const setTrack = (trackId) => {
(dispatch, getState) => {
const { audioObj } = getState();
if (audioObj != null) {
audioObj.unload();
}
dispatch({
audioObj: new Howler({
[ ... some options ],
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS
});
},
onloaderror: () => {
dispatch({
type: PLAYER_LOAD_ERROR
});
}
}),
type: PLAYER_SET_TRACK
});
}
you can move your other async and Howler work into action creators this way and make your reducer only concerned with storing information.

Mock local variable derived from redux store redux saga testing

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`
});

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