Redux saga testing using runSaga not updating the state - reactjs

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"

Related

Who to load dropdown options from API in react JS with typescript and react saga?

Here is my page, Here I want to load brand option from API.
I have written saga attached below:
Action.tsx
export const getBrandsForDropdown = (request: IPagination) => {
return {
type: actions,
payload: request
}
}
Api.tsx
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
console.log("get brand drop down");
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null }).then(
(data) => {
console.log("get brand drop down in ");
return { data, error: null };
},
(error) => {
return { data: null, error };
}
);
};
Reducer.ts
case actions.GET_BRANDS_DROPDOWN_PENDING:
return {
...state,
loading: true,
};
case actions.GET_BRANDS_DROPDOWN_REJECTED:
return {
...state,
loading: false,
};
case actions.GET_BRANDS_DROPDOWN_RESOLVED:
return {
...state,
loading: false,
brandOptions: action.payload,
};
Saga.ts
function* getBrandForDropDownSaga(action: HandleGetBrandsForDropdown) {
yield put(switchGlobalLoader(true));
yield put(pendingViewBrand());
try {
const { data } = yield getBrandsForDropdown();
yield put(resolvedViewBrand(data));
yield put(switchGlobalLoader(false));
} catch (error) {
yield put(switchGlobalLoader(false));
return;
}
}
After this I don't how to call it in my page and get it as a options in brand dropdown
Original Answer: Just Use Thunk
You can do this with redux-saga but I wouldn't recommend it. redux-thunk is a lot easier to use. Thunk is also built in to #reduxjs/toolkit which makes it even easier.
There is no need for an IPagination argument because you are always setting the pagination to {page: 1, limit: 1000}
Try something like this:
import {
createAsyncThunk,
createSlice,
SerializedError
} from "#reduxjs/toolkit";
import { IDropdownOption } from "office-ui-fabric-react";
import client from ???
// thunk action creator
export const fetchBrandsForDropdown = createAsyncThunk(
"fetchBrandsForDropdown",
async (): Promise<IDropdownOption[]> => {
const query = `user/master/brands?page=1&limit=1000`;
return client(query, { body: null });
// don't catch errors here, let them be thrown
}
);
interface State {
brandOptions: {
data: IDropdownOption[];
error: null | SerializedError;
};
// can have other properties
}
const initialState: State = {
brandOptions: {
data: [],
error: null
}
};
const slice = createSlice({
name: "someName",
initialState,
reducers: {
// could add any other case reducers here
},
extraReducers: (builder) =>
builder
// handle the response from your API by updating the state
.addCase(fetchBrandsForDropdown.fulfilled, (state, action) => {
state.brandOptions.data = action.payload;
state.brandOptions.error = null;
})
// handle errors
.addCase(fetchBrandsForDropdown.rejected, (state, action) => {
state.brandOptions.error = action.error;
})
});
export default slice.reducer;
In your component, kill the brandOptions state and access it from Redux. Load the options when the component mounts with a useEffect.
const brandOptions = useSelector((state) => state.brandOptions.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBrandsForDropdown());
}, [dispatch]);
CodeSandbox Link
Updated: With Saga
The general idea of how to write the saga is correct in your code.
take the parent asynchronous action.
put a pending action.
call the API to get data.
put a resolved action with the data or a rejected action with an error.
The biggest mistakes that I'm seeing in your saga are:
Catching errors upstream.
Mismatched data types.
Not wrapping API functions in a call effect.
Error Handling
Your brands.api functions are all catching their API errors which means that the Promise will always be resolved. The try/catch in your saga won't have errors to catch.
If you want to catch the errors in the saga then you need to remove the catch from the functions getBrandsForDropdown etc. You can just return the data directly rather than mapping to { result: data, error: null }. So delete the whole then function. I recommend this approach.
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null });
}
If you want to keep the current structure of returning a { result, error } object from all API calls then you need to modify the saga to look for an error in the function return.
function* getBrandForDropDownSaga() {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
const { data, error } = yield call(getBrandsForDropdown);
if (error) {
yield put(rejectedGetBrands(error.message));
} else {
yield put(resolvedGetBrands(data));
}
yield put(switchGlobalLoader(false));
}
Mismatched Data Types
There's some type mismatching in your reducer and state that you need to address. In some places you are using an array IBrand[] and in others you are using an object { results: IBrand[]; totalItems: number; currentPage: string; }. If you add the return type IState to the reducer then you'll see.
There's also a mismatch between a single IBrand and an array. I don't know the exact shape of your API response, but getBrandsForDropdown definitely has an array of brands somewhere. Your saga getBrandForDropDownSaga is dispatching resolvedViewBrand(data) which takes a single IBrand instead of resolvedGetBrands(data) which takes an array IBrand[]. If you add return types to the functions in your brands.api file then you'll see these mistakes highlighted by Typescript.
Don't Repeat Yourself
You can do a lot of combining in your API and your saga between the getBrands and the getBrandsForDropdown. Getting the brands for the dropdown is just a specific case of getBrands where you set certain arguments: { page: 1, limit: 1000 }.
export interface IPagination {
page?: number;
limit?: number;
sort?: "ASC" | "DESC";
column?: string;
}
export const getBrands = async (request: IPagination): Promise<IBrands> => {
const res = await axios.get<IBrands>('/user/master/brands', {
params: request,
});
return res.data;
};
function* coreGetBrandsSaga(request: IPagination) {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
try {
const data = yield call(getBrands, request);
yield put(resolvedGetBrands(data));
} catch (error) {
yield put(rejectedGetBrands(error?.message));
}
yield put(switchGlobalLoader(false));
}
function* getBrandsSaga(action: HandleGetBrands) {
const { sort } = action.payload;
if ( sort ) {
yield put(setSortBrands(sort));
// what about column?
}
const brandsState = yield select((state: AppState) => state.brands);
const request = {
// defaults
page: 1,
limit: brandsState.rowsPerPage,
column: brandsState.column,
// override with action
...action.payload,
}
// the general function can handle the rest
yield coreGetBrandsSaga(request);
}
function* getBrandsForDropDownSaga() {
// handle by the general function, but set certain the request arguments
yield coreGetBrandsSaga({
page: 1,
limit: 1000,
sort: "ASC",
column: "name",
})
}
export default function* brandsSaga() {
yield takeLatest(HANDLE_GET_BRANDS, getBrandsSaga);
yield takeLatest(GET_BRANDS_DROPDOWN, getBrandForDropDownSaga);
...
}
CodeSandbox

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.

Is there a way to mock refetch in MockedProvider - Apollo Client?

Here is how I am using MockedProvider. How can I mock refetch in mocks array?
const mocks = [{
request: {
query: GET_USERS_BY_FACILITY,
variables: {
facility: 300
}
},
result: {
data: {
GetUsersByFacility: [{
nuId: 'Q916983',
userName: faker.internet.userName(),
profileKey: 'testKey',
profileValue: 'testValue',
__typename: 'FacilityUser'
}]
}
},
refetch: () => {
return {
data: {
GetUsersByFacility: [{
nuId: 'Q916983',
userName: faker.internet.userName(),
profileKey: 'testKey',
profileValue: 'testValue',
__typename: 'FacilityUser'
}]
}
}
}
}
This test case calls refetch function when delete event is triggered.
it('should be able to click on delete user', async () => {
const {getByTestId} = render(
<MockedProvider mocks={mocks}>
<Users selectedFacility={300}/>
</MockedProvider>)
await wait(0)
fireEvent.click(getByTestId('btnDelete'))
})
I have been trying different ways, none seems to work. I get error message as TypeError: Cannot read property 'refetch' of undefined.
Thank you very much in hope of an answer.
Regards,
--Rajani
Maybe it's a bit late to answer, but if you have't got any answers yet, you would refer to the way I solved.
Please note that this might not be the correct answer.
You can find this code in react-apollo docs
const mocks = [
{
request: {
query: GET_DOG_QUERY,
variables: {
name: 'Buck',
},
},
result: () => {
// do something, such as recording that this function has been called
// ...
return {
data: {
dog: { id: '1', name: 'Buck', breed: 'bulldog' },
},
}
},
},
];
I make my refetch testcode based on this phrase // do something, such as recording that this function has been called
This is my mock example.
let queryCalled = false
const testingData = (value) => ({
data: {....}
})
const TESTING_MOCK = {
request: {
query: TESTING_QUERY,
variables: { some: "variables" },
},
result: () => {
if (queryCalled) return testingData("refetched");
else {
queryCalled = true;
return testingData("first fetched");
}
},
};
This component refetches data when the button is clicked. I designed my test code in this order
when it's rendered for the first time, it fetches the mock data .
=> In the code above, queryCalled is false so, it reassigns queryCalled as true and return the "first fetched" mock data,
when a click event occurs, refetch occurs too.
=> On the same principle the mock data returns "refetched" mock data.
My testcode example is here
it("refetch when clicked save button.", async () => {
const mocks = [TESTING_MOCK];
let utils: RenderResult = render(<SomeTestingComponent mocks={mocks} />);
await waitForNextTick(); //for getting a data, not loading
const titleInput = utils.getByDisplayValue("first fetched");
const saveBtn = utils.getByText("save");
fireEvent.click(saveBtn);
await waitForElement(() => utils.getByDisplayValue("refetched"));
})
Please let me know if you have any other suggestions!
For anyone that might still run into this, the solution to make refetch work in your tests is to use the newData method while keeping track of the query having been called.
I don't know if this is a bug in the MockedProvider implementation, but I was banging my head against the wall trying to make newData work together with result, but it turns out that newData completely overrides result.
A working solution (tested with useQuery and Apollo Client 3) would be something like this:
let queryCalled = false;
const refetchMock = {
request: {
query: YOUR_QUERY
},
newData: () => {
if (queryCalled) {
return {
data: {
// your refetched data
}
};
} else {
queryCalled = true;
return {
data: {
// your first fetched data
}
};
}
}
};
The newData solution didn't work for me with apollo client #2.6.
As a workaround, for the few tests that utilize refetch I had to physically mock the useQuery function and provide mock functions for the return of refetch; for our custom hook (where an overridden useQuery hook is exported as default), it looked something like this:
import * as useQueryModule from '~/hooks/useQuery';
describe('test the thing', () => {
let useQuerySpy;
beforeEach(() => {
// Spy on the `useQuery` function so we can optionally provide alternate behaviour.
useQuerySpy = jest.spyOn(useQueryModule, 'default');
})
afterEach(() => {
// Restore any mocked behaviour
useQuerySpy.mockRestore();
});
it('does a thing', async () => {
const refetchedApolloResponse = buildResponse(refetchData) // some function to build the shape / data of apollo response
const initialApolloResponse = buildResponse(initialData) // some function to build the shape / data of apollo response
const mockRefetch = jest.fn().mockResolvedValue({ data: refetchedApolloResponse });
useQuerySpy.mockReturnValue({ data: initialApolloResponse, refetch: mockRefetch });
// Assert stuff
}
})
This solution did not work for me and not sure whether it will work or not because it didn't work in my case.
let queryCalled = false
const testingData = (value) => ({
data: {....}
})
const TESTING_MOCK = {
request: {
query: TESTING_QUERY,
variables: { some: "variables" },
},
result: () => {
if (queryCalled) return testingData("refetched");
else {
queryCalled = true;
return testingData("first fetched");
}
},
};
So I solved it by another way which is:
const mocks = [
{
request: {
query: GET_DOG_QUERY,
variables: {
name: 'Buck',
},
},
result: {
data: {
dog: { id: '1', name: 'Buck', breed: 'bulldog' },
},
},
newData: jest.fn(() => ({
data: {
dog: { id: '1', name: 'Refetched-Buck', breed: 'refetched-bulldog' },
},
})),
},
];
It worked like a charm for me.

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 fetch action in react/redux app

Im starting with unit testing and Jest. What I want is to test the action's response after fetching some resources from the db.
This is the action code:
export function loadPortlets() {
return function(dispatch) {
return portletApi.getAllPortlets().then(response => {
dispatch(loadPortletsSuccess(response));
dispatch(hideLoading());
}).catch(error => {
dispatch({ type: null, error: error });
dispatch(hideLoading());
throw(error);
});
};
}
This code is fetching data from:
static getAllPortlets() {
return fetch(`${API_HOST + API_URI}?${RES_TYPE}`)
.then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
})
);
}
And this is the test:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetch from 'isomorphic-fetch';
import fetchMock from 'fetch-mock';
import * as actions from '../portletActions';
import * as types from '../actionTypes';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
'Content-type': 'application/json'
}
});
};
describe('async actions', () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
})
it('calls request and success actions if the fetch response was successful', () => {
window.fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, [{ portlets: ['do something'] }])));
const store = mockStore({ portlets: []});
return store.dispatch(actions.loadPortlets())
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions[0]).toContain({ type: types.LOAD_PORTLETS_SUCCESS });
})
});
});
And this is the result of running the test:
FAIL src\actions\__tests__\portletActions.tests.js
● async actions › calls request and success actions if the fetch response was successful
expect(object).toContain(value)
Expected object:
{"portlets": [// here an array of objects], "type": "LOAD_PORTLETS_SUCCESS"}
To contain value:
{"type": "LOAD_PORTLETS_SUCCESS"}
at store.dispatch.then (src/actions/__tests__/portletActions.tests.js:56:34)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
In the redux docs for this example (https://redux.js.org/recipes/writing-tests), they receive a result containing only the action types executed, but I'm getting the real data and the action inside the array.
So I'm not sure if the code is wrong, or the test, or both!
Thanks in advance, any help is highly appreciated!
You're testing too much with this unit test. I see you are using thunks it looks like so you can change your fetch to be passed as a module to the thunk and do something like this. I used jasmine but it's basically the same thing. You don't want to mock your store here just the action and dispatch. The point of the unit test should be to test the async action, not to test getting real data from the db or redux store interactions so you can stub all that out.
For reference configureStore would look like this...
const createStoreWithMiddleware = compose(
applyMiddleware(thunk.withExtraArgument({ personApi }))
)(createStore);
And the test case...
it('dispatches an action when receiving', done => {
const person = [{ firstName: 'Francois' }];
const expectedAction = {
type: ActionTypes.RECEIVED,
payload: {
people,
},
};
const dispatch = jasmine.createSpy();
const promise = Q.resolve(person);
const personApi = {
fetchPerson: jasmine
.createSpy()
.and.returnValue(promise),
};
const thunk = requestPerson();
thunk(dispatch, undefined, { personApi });
promise.then(() => {
expect(dispatch.calls.count()).toBe(2);
expect(dispatch.calls.mostRecent().args[0]).toEqual(expectedAction);
done();
});
});

Resources