Getting an Async action creator test to pass Redux - javascript

I have an async actionCreator which sends off an API call - on success it gives the reponse on failure - it fails. I am writing tests for the call but I can't get the test to send back the correct response.
Here is the function I am testing - Each dispatch dispatches an action.
const getStudyData = () => {
return async dispatch => {
try {
dispatch(fetchStudiesBegin());
const res = await DataService.fetchAllStudyData();
dispatch(fetchStudiesSuccess(res))
}
catch (err) {
dispatch(fetchStudiesError(err))
}
}
}
const fetchStudiesBegin = () => ({
type: types.FETCH_STUDIES_BEGIN
});
const fetchStudiesSuccess = studies => ({
type: types.FETCH_STUDIES_SUCCESS,
payload: { studies }
});
const fetchStudiesError = error => ({
type: types.FETCH_STUDIES_ERROR,
payload: { error }
});
This is the test that I have written - It is however giving me the ERROR response instead of the SUCCESS response
import configureStore from 'redux-mock-store';
const middlewares = [ thunk ];
const mockStore = configureStore(middlewares);
import fetchMock from 'fetch-mock';
describe('Test thunk action creator for the API call for studies ', () => {
it('expected actions should be dispatched on successful request', () => {
const store = mockStore({});
const expectedActions = [
types.FETCH_STUDIES_BEGIN,
types.FETCH_STUDIES_SUCCESS
];
// Mock the fetch() global to always return the same value for GET
// requests to all URLs.
fetchMock.get('*', { response: 200 });
return store.dispatch(dashboardOperations.getStudyData())
.then(() => {
const actualActions = store.getActions().map(action => action.type);
expect(actualActions).toEqual(expectedActions);
});
fetchMock.restore();
});
});
});

Related

Jest - Test catch block in an asyncThunk (Redux toolkit)

I've been struggling recently because I'm trying to fix some tests that another developer made before he left the company I'm working on. It involves testing a catch block inside a createAsyncThunk, the thunk was created as follows:
export const onEmailSubmit = createAsyncThunk(
'email/onEmailSubmit',
async (data, { dispatch, rejectWithValue, getState }) => {
dispatch(updateEmail({ isLoadingEmailRequest: true }))
try {
const response = await updateIdentity(data)
return response.data
} catch (err) {
// If the error doesn't have any status code, there is a Network Error
// so let's show the error modal:
if (err.request) {
dispatch(updateErrorModal({ showErrorModal: true }))
}
return rejectWithValue(err.data)
}
})
The test file is something along the lines:
import userReducer, { updateEmail, handleEmailVerify } from './EmailSlice'
import configureStore from 'redux-mock-store' // ES6 modules
import thunk from 'redux-thunk'
import { updateIdentity } from '../../../http'
const middlewares = [thunk]
const mockStore = configureStore(middlewares)
jest.mock('../../../http/', () => ({
...jest.requireActual('../../../http/'),
updateIdentity: jest.fn().mockImplementation((data) => {
return Promise.resolve({
simpleFieldsValues: {
displayName: 'test',
language: 'en_US'
}
})
})
}))
describe('EmailSlice unit tests', () => {
beforeEach(() => {
jest.clearAllMocks()
Object.defineProperty(window, 'sessionStorage', {
value: {
getItem: jest.fn(() => null),
setItem: jest.fn(() => null),
removeItem: jest.fn(() => null)
},
writable: true
})
})
afterEach(() => {
jest.restoreAllMocks()
})
// ...Some tests before this one
it('tests when handleEmailVerify is called and backend does not return a response', async () => {
const error = new Error()
error.request = {
message: 'test'
}
sendVerificationCode.mockImplementation(_ => Promise.reject(error))
const store = mockStore({})
await store.dispatch(handleEmailVerify())
const actions = store.getActions()
const rejectedActionLoading = actions[1]
expect(rejectedActionLoading.type).toEqual(updateEmail.type)
expect(rejectedActionLoading.payload).toEqual({ isLoadingEmailRequest: true })
const rejectedAction = actions[2]
expect(rejectedAction.type).toEqual("errorModal/updateErrorModal")
})
})
The thing is that the tests fail because the dispatch to update the error modal is never reached. I think this is related to the fact that the mocked promise sendVerificationCode.mockImplementation(_ => Promise.reject(error)) does not return the object with the request object inside of it to the catch block, thus not dispatching the updateErrorModal.
PS: If I remove the if statement and just dispatch the updateErrorModal, the test passes.
Do you guys have any idea how to fix this?
Thanks for your time :)

Spy on mock service worker (msw)?

I'm starting to use the msw (mock service worker) after watching this example of how to use it for testing API calls in React applications.
Is there any way that we can spy on the mock service worker?
For example:
import React from 'react'
import { render, act, await } from '#testing-library/react'
import userEvent from '#testing-library/user-event'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import SearchBox from '.'
const fakeServer = setupServer(
rest.get(
'https://api.flickr.com/services/rest/?method=flickr.photos.search',
(req, res, ctx) => res(ctx.status(200), ctx.json({ data: { photos: { photo: [] },},}))
)
)
beforeAll(() => {fakeServer.listen()})
afterEach(() => {fakeServer.resetHandlers()})
afterAll(() => fakeServer.close())
test('it calls Flickr REST request when submitting search term', async () => {
const { getByLabelText } = render(<SearchBox />)
const input = getByLabelText('Search Flickr')
const submitButton = getByLabelText('Submit search')
await act(async () => {
await userEvent.type(input,'Finding Wally')
await userEvent.click(submitButton)
})
await wait()
// TODO: assert that the fakeServer was called once and with the correct URL
})
The component to test looks like this:
import React, { useState } from 'react'
import axios from 'axios'
import './index.css'
function SearchBox({ setPhotos }) {
const [searchTerm, setSearchTerm] = useState('')
const handleTyping = (event) => {
event.preventDefault()
setSearchTerm(event.currentTarget.value)
}
const handleSubmit = async (event) => {
event.preventDefault()
try {
const restURL = `https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=${
process.env.REACT_APP_API_KEY
}&per_page=10&format=json&nojsoncallback=1'&text=${encodeURIComponent(
searchTerm
)}`
const { data } = await axios.get(restURL)
const fetchedPhotos = data.photos.photo
setPhotos(fetchedPhotos)
} catch (error) {
console.error(error)
}
}
return (
<section style={styles.container}>
<form action="" method="" style={styles.form}>
<input
aria-label="Search Flickr"
style={styles.input}
value={searchTerm}
onChange={handleTyping}
/>
<button
aria-label="Submit search"
style={styles.button}
onClick={handleSubmit}
>
SEARCH
</button>
</form>
</section>
)
}
I have got a working test, but I feel it leans towards an implementation test since it uses a spy on the setPhotos
test('it calls Flickr REST request when submitting search term', async () => {
const fakeSetPhotos = jest.fn(() => {})
const { getByLabelText } = render(<SearchBox setPhotos={fakeSetPhotos} />)
const input = getByLabelText('Search Flickr')
const submitButton = getByLabelText('Submit search')
await act(async () => {
await userEvent.type(input, 'Finding Walley')
await userEvent.click(submitButton)
})
await wait()
expect(fakeSetPhotos).toHaveBeenCalledWith([1, 2, 3])
})
The devs at mswjs are really nice and helpful. They took their time to advice me on how to approach it.
TLDR;
The current working test I got is fine - just suggested an alternative to jest.fn() - I do like the readability of the their suggestion:
test('...', async () => {
let photos
// Create an actual callback function
function setPhotos(data) {
// which does an action of propagating given data
// to the `photos` variable.
photos = data
}
// Pass that callback function as a value to the `setPhotos` prop
const { getByLabelText } = render(<SearchBox setPhotos={setPhotos} />)
// Perform actions:
// click buttons, submit forms
// Assert result
expect(photos).toEqual([1, 2, 3])
})
Another thing I wanted to test was that it actually calls a valid REST URL.
You can reflect an invalid query parameter in the response resolver.
If the query parameter is missing/invalid your real server would not
produce the expected data, right? So with MSW your "real server" is
your response resolver. Check the presence or value of that query
parameter and raise an error in case that parameter is invalid.
rest.get('https://api.flickr.com/services/rest/?method=flickr.photos.search',
(req, res, ctx) => { const method = req.url.searchParams.get('method')
if (!method) {
// Consider a missing `method` query parameter as a bad request.
return res(ctx.status(400)) }
// Depending on your logic, you can also check if the value of the `method` // parameter equals to "flickr.photos.search".
return res(ctx.json({ successful: 'response' })) })
Now, if your app misses the method query parameter in the request URL, it would get a 400 response, and shouldn't call the setPhotos callback in case of such unsuccessful response.
If you want to avoid mocking you could spy on axios.get and assert that it was called correctly.
test('it calls Flickr REST request when submitting search term', async () => {
const getSpy = jest.spyOn(axios, 'get');
const { getByLabelText } = render(<SearchBox />)
const input = getByLabelText('Search Flickr')
const submitButton = getByLabelText('Submit search')
await act(async () => {
await userEvent.type(input,'Finding Wally')
await userEvent.click(submitButton)
})
await wait()
expect(getSpy).toHaveBeenCalledTimes(1)
})

How to test catch statement in async await Action

Problem
I have an Action which awaits an API function. The happy path in the try is easily testable with my mocked API. However, unsure as to the best way to test and cover the .catch.
Actions
import {getRoles} from '../shared/services/api';
export const Actions = {
SET_ROLES: 'SET_ROLES'
};
export const fetchRoles = () => async dispatch => {
try {
const response = await getRoles();
const roles = response.data;
dispatch({
type: Actions.SET_ROLES,
roles
});
} catch (error) {
dispatch({
type: Actions.SET_ROLES,
roles: []
});
}
};
Actions Test
import {fetchRoles} from '../party-actions';
import rolesJson from '../../shared/services/__mocks__/roles.json';
jest.mock('../../shared/services/api');
describe('Roles Actions', () => {
it('should set roles when getRoles() res returns', async () => {
const mockDispatch = jest.fn();
await fetchRoles()(mockDispatch);
try {
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_ROLES',
roles: rolesJson
});
} catch (e) {
// console.log('fetchRoles error: ', e)
}
});
// Here is the problem test, how do we intentionally cause
// getRoles() inside of fetchRoles() to throw an error?
it('should return empty roles if error', async () => {
const mockDispatch = jest.fn();
await fetchRoles('throwError')(mockDispatch);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_ROLES',
roles: []
});
});
});
Mocked API
import rolesJson from './roles.json';
export const getRoles = async test => {
let mockGetRoles;
if (test === 'throwError') {
// console.log('sad')
mockGetRoles = () => {
return Promise.reject({
roles: []
});
};
} else {
// console.log('happy')
mockGetRoles = () => {
return Promise.resolve({
roles: rolesJson
});
};
}
try {
const roles = mockGetRoles();
// console.log('api mocks roles', roles);
return roles;
} catch (err) {
return 'the error';
}
};
^ Above you can see what I tried, which did work, but it required me to change my code in a way that fit the test, but not the actual logic of the app.
For instance, for this test to pass, I have to pass in a variable through the real code (see x):
export const fetchRoles = (x) => async dispatch => {
try {
const response = await getRoles(x);
const roles = response.data;
How can we force getRoles in our mock to throw an error in our sad path, .catch test?
You can mock getRoles API on per-test basis instead:
// getRoles will be just jest.fn() stub
import {getRoles} from '../../shared/services/api';
import rolesJson from '../../shared/services/__mocks__/roles.json';
// without __mocks__/api.js it will mock each exported function as jest.fn();
jest.mock('../../shared/services/api');
it('sets something if loaded successfully', async ()=> {
getRoles.mockReturnValue(Promise.resolve(rolesJson));
dispatch(fetchRoles());
await Promise.resolve(); // so mocked API Promise could resolve
expect(someSelector(store)).toEqual(...);
});
it('sets something else on error', async () => {
getRoles.mockReturnValue(Promise.reject(someErrorObject));
dispatch(fetchRoles());
await Promise.resolve();
expect(someSelector(store)).toEqual(someErrornessState);
})
I also propose you concentrate on store state after a call not a list of actions dispatched. Why? Because actually we don't care what actions in what order has been dispatched while we get store with data expected, right?
But sure, you still could assert against dispatch calls. The main point: don't mock result returned in __mocks__ automocks but do that on peer-basis.
I resolved the test and got the line coverage for the .catch by adding a function called mockGetRolesError in the mock api file:
Thanks to #skyboyer for the idea to have a method on the mocked file.
import {getRoles} from '../shared/services/api';
export const Actions = {
SET_ROLES: 'SET_ROLES'
};
export const fetchRoles = () => async dispatch => {
try {
const response = await getRoles();
const roles = response.data;
// console.log('ACTION roles:', roles);
dispatch({
type: Actions.SET_ROLES,
roles
});
} catch (error) {
dispatch({
type: Actions.SET_ROLES,
roles: []
});
}
};
Now in the test for the sad path, I just have to call mockGetRolesError to set the internal state of the mocked api to be in a return error mode.
import {fetchRoles} from '../party-actions';
import rolesJson from '../../shared/services/__mocks__/roles.json';
import {mockGetRolesError} from '../../shared/services/api';
jest.mock('../../shared/services/api');
describe('Roles Actions', () => {
it('should set roles when getRoles() res returns', async () => {
const mockDispatch = jest.fn();
try {
await fetchRoles()(mockDispatch);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_ROLES',
roles: rolesJson
});
} catch (e) {
return e;
}
});
it('should return empty roles if error', async () => {
const mockDispatch = jest.fn();
mockGetRolesError();
await fetchRoles()(mockDispatch);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_ROLES',
roles: []
});
});
});

Mocking Axios in React Hooks using react-hooks-testing-library

Trying to mock GET request to API but always get
Timeout - Async callback was not invoked within the 10000ms timeout specified by jest.setTimeout.
even though I increased the timeout it still throws error.
Hook
export default function apiCaller() {
const [rawApiData, setRawApiData] = useState({});
const [errorMsg, setErrorMsg] = useState('');
const callApi = async (inputValue) => {
try {
const apiData= await axios.get(
`https://cloud.iexapis.com/stable/stock/market/batch?types=chart&symbols=${inputValue}&range=3m&token=lalaccf0`
);
setRawApiData(apiData);
} catch (err) {
setErrorMsg(
'Error occured!! ' +
(Boolean(err.response) ? err.response.data : err.message)
);
}
};
return { rawApiData, callApi, errorMsg };
}
Axios mock
export default {
get: jest.fn().mockResolvedValue({ data: {} }),
};
Test
import { renderHook, act } from 'react-hooks-testing-library';
import apiCaller from '../components/stock-chart/stockApiCaller';
import axios from 'axios';
jest.mock('axios');
it('should set error properly when api call is unsuccessfull because of bad data', async () => {
axios.get.mockResolvedValueOnce({ data: { test: '123' } });
const { result, waitForNextUpdate } = renderHook(() => apiCaller());
act(() => result.current.callApi('fb/tsla'));
await waitForNextUpdate();
expect(result.current.rawApiData.data.test)
.toBe(123)
}, 10000);
I finally got the issue resolved. There is new way to write act() i.e. async act(). Please find below the updated version of test which works fine.
it('should set rawData properly when api call is successfull because of', async () => {
axios.get.mockResolvedValueOnce({ data: { test: '123' } });
const { result, waitForNextUpdate } = renderHook(() => apiCaller());
await act(async () => {
result.current.callApi('fb/tsla');
await waitForNextUpdate();
});
expect(result.current.rawApiData.data.test).toBe('123');
});
Update react to 16.9.0-alpha.0
https://github.com/facebook/react/releases/tag/v16.9.0-alpha.0

Why can't I dispatch an action when a promise resolves inside Redux middleware?

Background
I am writing a piece of Redux middleware that makes an axios request and uses Cheerio to parse the result.
Problem
When the Axios promise resolves and I try to dispatch a fulfilled action the action does not show up in the store's action log in the test.
Middleware
function createMiddleware() {
return ({ dispatch, getState }) => next => action => {
if (isCorrectAction(action)===true){
const pendingAction = {
type: `${action.type}_PENDING`,
payload: action.payload
}
dispatch(pendingAction)
axios.get(action.payload.url)
.then(response => {
let $ = cheerio.load(response.data)
let parsedData = action.payload.task($)
const fulfilledAction = {
type: `${action.type}_FULFILLED`,
payload: {
parsedData
}
}
dispatch(fulfilledAction) // dispatch that is problematic
})
.catch( err => {
});
}
return next(action);
}
}
Test that fulfilled action is dispatched fails
it('should dispatch ACTION_FULFILLED once', () => {
nock("http://www.example.com")
.filteringPath(function(path) {
return '/';
})
.get("/")
.reply(200, '<!doctype html><html><body><div>text</div></body></html>');
const expectedActionTypes = ['TASK', 'TASK_PENDING', 'TASK_FULFILLED']
// Initialize mockstore with empty state
const initialState = {}
const store = mockStore(initialState)
store.dispatch(defaultAction)
const actionsTypes = store.getActions().map(e => e.type)
expect(actionsTypes).has.members(expectedActionTypes);
expect(actionsTypes.length).equal(3);
});
Solution - Promise needs to be returned in the mocha test
The solution is to rewrite the mocha test so that the promise is returned. I mistakenly thought that by using nock to intercept the HTTP request that the promise would become synchronous.
The working test looks like:
it('should dispatch ACTION_FULFILLED once', () => {
nock("http://www.example.com")
.filteringPath(function(path) {
return '/';
})
.get("/")
.reply(200, '<!doctype html><html><body><div>text</div></body></html>');
const store = mockStore();
return store.dispatch(defaultScrapingAction)
.then(res => {
const actionsTypes = store.getActions().map(e => e.type)
expect(actionsTypes).has.members(expectedActionTypes);
expect(actionsTypes.length).equal(3);
})
});

Categories