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

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.

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"

Facebook and Google requires two login click to login with Firebase Auth

I have React web application with firebase auth (mail, Facebook, Google).
Google and Facebook work only after 2 login clicks.
The code is equal, just the provider is different.
import React from 'react';
import firebase from "firebase/app";
import { app } from "../../../../config/firebase";
const signupWithGoogle = (user, userInfo)=>{
app.firestore().collection('users').doc(user.uid).set({
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name});
const batch = app.firestore().batch();
const initData = [
{ Applied: { positionIds: [], title: 'Applied' } },
{ Contract: { positionIds: [], title: 'Contract' } },
{ Denied: { positionIds: [], title: 'Denied' } },
{ InProgress: { positionIds: [], title: 'In Progress' } },
{ ReceivedTask: { positionIds: [], title: 'Received Task' } },
];
initData.forEach((doc) => {
const docRef = app
.firestore()
.collection('users')
.doc( user.uid)
.collection('columns')
.doc(Object.keys(doc)[0]);
batch.set(docRef, Object.values(doc)[0]);
});
const batchCommit= batch.commit();
return batchCommit;
}
export const googleLogin = async (
history
) => {
var provider = new firebase.auth.GoogleAuthProvider();
await firebase.auth()
.signInWithPopup(provider)
.then( resp => {
let {user, credential,additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser) signupWithGoogle(user, userInfo);
}).then(()=>
history.push('/')
)
.catch((error) => {
console.error(error.message);
});
};
I saw this question, but didn't help.(Firebase Authentication Requires Two 'Login' Calls)
I had the same problem with Firebase Authentication with Facebook, I had to register two times to make it works.
The problem was in my HTLM, I used a form.
I changed for a simpler code, and it worked.
While waiting for where you call your function from, as your issue would relate to improper state management, here are some edits you can make to the code you have shared so far to squash some problems that it has.
In your signupWithGoogle function, you create a floating promise that should be included in the write batch that you use to create the /users/{userId}/columns collection. Because you use Object.keys(doc)[0] and Object.values(doc)[0], you should consider using an array of [docId, docData] pairs or a JSON-like object structure like so:
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
As you mentioned that a lot of your code is shared aside from the provider implementation, you should consider extracting the common code from those functions:
const initUserData = (user, userDocData) => {
// init write batch
const batch = app.firestore().batch();
// init ref to user data
const userDocRef = app.firestore().collection('users').doc(user.uid);
// queue user data upload
batch.set(userDocRef, userDocData);
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
// make the changes
return batch.commit();
}
const initUserDataForGoogle(user, userInfo) {
return initUserData(user, {
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name
});
}
const initUserDataForFacebook(user, userInfo) {
return initUserData(user, {
firstName: /* ... */,
lastName: /* ... */
});
}
When exporting a function to be called elsewhere, avoid causing "side effects" (like navigating using the History API) and don't trap errors (using .catch() without rethrowing the error). The calling code should handle the result and any errors itself.
export const loginWithGoogle = async () => {
const provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth()
.signInWithPopup(provider)
.then(async resp => {
const {user, credential, additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser)
await initUserDataForGoogle(user, userInfo);
return user;
});
};
Then in your components, you'd use:
setLoading(true);
/* await/return */ loginWithGoogle()
.then(() => {
history.push('/');
// or
// setLoading(false)
// then do something
})
.catch((err) => {
console.error("loginWithGoogle failed: ", err);
setLoading(false);
setError("Failed to log in with Google!"); // <- displayed in UI to user
});

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.

nock for axios mockup on 400 does not return the defined value

I am using nock to mock my ajax call with axios.
Here is my code:
describe("unSuccessful rest call",()=>{
it('should dispatch types: SET_DROP_DOWN_CHECK_STATUS in order ', () => {
nock(getRootUrl(ServiceUrls.prototype.getContactUsUrl())).get(getExtention(ServiceUrls.prototype.getContactUsUrl())).replyWithError(400, "test");
const expectedActions = [
{
"type": SET_DROP_DOWN_CHECK_STATUS,
"payload": "test"
}
];
return store.dispatch(setContactUsList({push:()=>{}})).then(() => {
expect(store.getActions()[0]).to.eql(expectedActions[0]);
})
})
})
When I run the above test it hits the server and return the actual error message instead of test which I asked for.
Interestingly when I use the above code for 200 it successfully returns what I define.
Can anyone help what is wrong with my approach?
Not sure what is wrong with nock as I tried to use it and ran into similar issues. I use axios-mock-adapter and found that a simpler tool to work with.
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
it('should fail when trying to search for documents', async () => {
mock.onGet(SEARCH_ENDPOINT + formattedFields).reply(400, {
details: [
{
code: 400,
message: 'No claim number or DCN provided'
}
]
});
const given = { fields: activeFields };
const expected = [
{ type: types.FETCH_DOCS_STARTED },
{ type: types.FETCH_DOCS_FAILED, message: 'No claim number or DCN provided' }
];
await store.dispatch(actions.fetchDocs(given));
const actualDispatchedActions = store.getActions();
expect(actualDispatchedActions).toEqual(expected);
});

Redux-observable: failed jest test for epic

I followed the steps from documentation to test epic.
...
store.dispatch({ type: FETCH_USER });
expect(store.getActions()).toEqual([
{ type: FETCH_USER },
{ type: FETCH_USER_FULFILLED, payload }
]);
...
But I get failed because second action is been received some later like following.
Test failed
Expected value to equal:
[{"type": "FETCH_USER"}, {"type": "FETCH_USER_FULFILLED", "payload": [some]}]
Received:
[{"type": "FETCH_USER"}]
Difference:
- Expected
+ Received
## -1,20 +1,5 ##
Array [
Object {"type": "FETCH_USER"},
Object {"type": "FETCH_USER_FULFILLED", "payload": [some]} ] // this is what should be.
So I think I should know when the dispatch is finished or some like that.
How can I solve this?
I used fetch() and Rx.Observable.fromPromise instead of ajax.getJSON()
Here is my epic.
const fetchUserEpic = (action$) =>
action$
.ofType(FETCH_USER)
.mergeMap(() => {
return Rx.Observable.fromPromise(api.fetchUser())
.map((users) => ({
type: FETCH_USER_FULFILLED,
payload: { users }
}))
.catch((error) => Rx.Observable.of({
type: FETCH_USER_ERROR,
payload: { error }
}))
.takeUntil(action$.ofType(FETCH_USER_CANCELLED))
})
The reason is that promises always resolve on the next microtask so your api.fetchUser() isn't emitting synchronously.
You'll need to either mock it out, use something like Promise.resolve().then(() => expect(store.getActions).toEqual(...) to wait until the next microtask, or you can experiment with testing your epics directly without using redux.
it('Epics with the appropriate input and output of actions', (done) => {
const action$ = ActionsObservable.of({ type: 'SOMETHING' });
somethingEpic(action$, store)
.toArray() // collects everything in an array until our epic completes
.subscribe(actions => {
expect(actions).to.deep.equal([
{ type: 'SOMETHING_FULFILLED' }// whatever actions
]);
done();
});
});
This will be our preferred testing story in the docs when I (or someone else) has time to write them up. So instead of using redux and the middleware in your tests, we just call the epic function directly with our own mocks. Much easier and cleaner.
With that approach, we can leverage the new dependency injection feature of redux-observable: https://redux-observable.js.org/docs/recipes/InjectingDependenciesIntoEpics.html
import { createEpicMiddleware, combineEpics } from 'redux-observable';
import { ajax } from 'rxjs/observable/dom/ajax';
import rootEpic from './somewhere';
const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { getJSON: ajax.getJSON }
});
// Notice the third argument is our injected dependencies!
const fetchUserEpic = (action$, store, { getJSON }) =>
action$.ofType('FETCH_USER')
.mergeMap(() =>
getJSON(`/api/users/${payload}`)
.map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
);
import { ActionsObservable } from 'redux-observable';
import { fetchUserEpic } from './somewhere/fetchUserEpic';
const mockResponse = { name: 'Bilbo Baggins' };
const action$ = ActionsObservable.of({ type: 'FETCH_USERS_REQUESTED' });
const store = null; // not needed for this epic
const dependencies = {
getJSON: url => Observable.of(mockResponse)
};
// Adapt this example to your test framework and specific use cases
fetchUserEpic(action$, store, dependencies)
.toArray() // buffers all emitted actions until your Epic naturally completes()
.subscribe(actions => {
assertDeepEqual(actions, [{
type: 'FETCH_USER_FULFILLED',
payload: mockResponse
}]);
});
First, use isomorphic-fetch instead of Observable.ajax for nock support, like this
const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
.then(res => res.json());
return Observable.from(request);
};
So my epic is:
const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
action$
.ofType(FETCH_MODEL)
.mergeMap((action: FetchModel) =>
fetchDynamicData(action.url, action.params)
.map((payload: FetchedData) => fetchModelSucc(payload.data))
.catch(error => Observable.of(
fetchModelFail(error)
)));
Then, you may need an interval to decide when to finish the test.
describe("epics", () => {
let store: MockStore<{}>;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(epic);
});
it("fetch data model succ", () => {
const payload = {
code: 0,
data: someData,
header: {},
msg: "ok"
};
const params = {
data1: 100,
data2: "4"
};
const mock = nock("https://test.com")
.get("/test")
.query(params)
.reply(200, payload);
const go = new Promise((resolve) => {
store.dispatch({
type: FETCH_MODEL,
url: "https://test.com/test",
params
});
let interval: number;
interval = window.setInterval(() => {
if (mock.isDone()) {
clearInterval(interval);
resolve(store.getActions());
}
}, 20);
});
return expect(go).resolves.toEqual([
{
type: FETCH_MODEL,
url: "https://test.com/assignment",
params
},
{
type: FETCH_MODEL_SUCC,
data: somData
}
]);
});
});
enjoy it :)

Resources