I have a react + typescript application and I have an async api call done with axios. I want to test that async call using Jest + Enzyme.
This is what I have for my action
// load items callback
export function loadItemsSuccess(response) {
return {
type: LOAD_ITEMS,
items: response.data
};
}
// load items
export function loadItems() {
return function (dispatch) {
const authOptions = {
method: "GET",
url: "http://192.168.99.100:3000/items
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
json: true
};
return axios(authOptions).then(response => {
dispatch(loadItemsSuccess(response));
}).catch(error => {
console.log("Error loading items, error);
});
};
}
My reducer simple updates the store:
case LOAD_ITEMS:
console.log(action.items);
return action.items;
Unit testing the action should test whether the expected object is dispatched.
One way to do this is to use a combination of redux-mock-store and axios-mock-adapter.
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
// import any actions and types
const middleware = [thunk]
const mockStore = configureMockStore(middleware)
describe('loadItems', () => {
it('Dispatches LOAD_ITEMS', () => {
let mock = new MockAdapter(axios)
mock
.onGet('http://192.168.99.100:3000/items')
.reply(200, { data: 'mock' })
const store = mockStore({})
return store.dispatch(actions.loadItems())
.then(() => {
const actions = store.getActions()
expect(actions[0]).toHaveProperty('type', LOAD_ITEMS)
})
})
})
Reducers and the values in the store should be tested as a separate unit. We're just using the mockStore to dispatch the action and ensure the proper type is dispatched. MockAdapter mocks network requests. This way we can create tests around various network conditions such as 400s and timeouts.
Axios Mock Adapter
Redux Mock Store
Related
I am using Redux Toolkit's createAsyncThunk for an API request. This specific thunk is calling a sepparate non-anonymous function as its second parameter:
export const onInfoSubmit = createAsyncThunk('info/onInfoSubmit', handlePayload)
The handlePayload function goes like:
export async function handlePayload (data, { dispatch, rejectWithValue, getState }) {
dispatch(updateInfo({ isLoadingInfoRequest: true }))
if (!isFeatureEnabled(FEATURE_NAME)) {
return handleUpdateInfo(data, { dispatch, rejectWithValue, getState })
}
return thirdPartyApiCall()
.then((response) => {
data.thirdPartyResponse = response
return handleUpdateInfo(data, { dispatch, rejectWithValue, getState })
})
}
The handleUpdateInfo function is used to set Session Storage items, send a PUT request with the dynamically created payload with an axios call and, in case of failures or in case of response errors, trigger reject accordingly.
My question is, how could I test if, for instance, calling mockStore.dispatch(onInfoSubmit) calls the handlePayload function, and further on (if handlePayload calls handleUpdateInfo, for instance)?
I have tried the following approaches:
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk]
const mockStore = configureStore(middlewares)
jest.mock('./InfoSlice.js', () => ({
...jest.requireActual('./InfoSlice'),
handlePayload: jest.fn((data) => new Promise((resolve, reject) => resolve(data))),
handleUpdateInfo: jest.fn((data) => new Promise((resolve, reject) => resolve(data)))
}))
describe('InfoSlice 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()
})
const initialState = {
loginInfo: {
userName: 'test#test.com',
email: 'test#test.com'
},
verifyEmailResponse: {},
email: {
loginInfo: {
email: 'test#test.com'
}
}
}
it('onInfoSubmit calls handlePayload', async () => {
const store = mockStore(initialState)
await store.dispatch(onEmailSubmit({
loginInfo: {
userName: 'new#test.com',
email: 'new#test.com',
}
}))
expect(handlePayload).toHaveBeenCalled()
})
But for some reason, handlePayload never gets called in this test.
Trying to check if handleUpdateInfo gets called in the same manner, also results in a broken test...
Besides, I cannot really seem to get any of the mock functions called on these tests.
Any tips on this issue will be much appreciated!
Technically, you are mocking the export with the name handlePayload - but inside that file, onInfoSubmit has still been created with the local function definition. You would probably have to split those over a file boundary to make that work.
Generally though, that test would not be testing anything besides the question if you have actually written handlePayload in that line - the test does not give you any confidence that your code is really working.
I'd recommend to mock as little as possible of your own code, but only mock things like apis (using msw for example) and window.localStorage. Then run your thunks in a real-life environment and see it your component works. We recommend doing real-life integration tests in the Redux docs as that will give you the most confidence on actual functionality, not just implementation details.
I can't understand why I'm getting this error. I am trying to use the Vuex store in a composition function but it keeps throwing me this error about inject (I'm not even using inject). My app makes an await api call to the backend and if there is an error calls my composition function.
[Vue warn]: inject() can only be used inside setup() or functional components.
inject # runtime-dom.esm-bundler-9db29fbd.js:6611
useStore # vuex.esm-bundler.js:13
useErrorHandling # useErrorHandling.js:5
checkUserExists # auth.js:53
Here is my composition function
import { useStore } from 'vuex'
function useErrorHandling()
{
const store = useStore() // <-- this line
function showError(errorMessage) {
console.log(errorMessage)
}
return { showError }
}
export default useErrorHandling
If I remove this line then it doesn't throw that error
// const store = useStore() // <-- this line
UPDATE: this is how the function is called.
/**
* Check if a user exists in database
*/
static async checkUserExists(data)
{
const { env } = useEnv()
const { jsonHeaders } = useHTTP()
const { showError } = useErrorHandling()
try {
let response = await fetch(`${env('VITE_SERVER_URL')}/auth/check-user-exists`, {
method: 'POST',
body: JSON.stringify(data),
headers: jsonHeaders,
})
if (!response.ok) {
let errorMessage = {
statusText: response.statusText,
statusCode: response.status,
body: '',
url: response.url,
clientAPI: 'api/auth.js # checkUserExists',
}
const text = await response.text()
errorMessage.body = text
showError(errorMessage) // <-- here
return
}
response = await response.json()
return response.user_exists
} catch (error) {
alert('Error occured!')
console.log(error)
}
}
The error is telling you that useStore is only for use inside of components, since the module isn't a component. From the docs:
To access the store within the setup hook, you can call the useStore function. This is the equivalent of retrieving this.$store within a component using the Option API.
To use the store in a module, you can import { store } from the module where it was created:
store.js
export const store = createStore({
...
})
Other module
import { store } from './store'
I am using Vuex/Axios to make GET requests to an API. When a component mounts, I am dispatching an action to the Vuex store and making the Axios GET request. In the Vuex action, the Axios GET request returns the response as expected but the response inside the component is returning undefined. What am I doing wrong?
axios/index.js
import axios from 'axios';
const API_URL = 'http://localhost:3000/api/v1/';
const plainAxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
export { plainAxiosInstance };
Vuex module: store/modules/character.js. In this file response logs the correct response. The fetchCharacters event gets triggered in a component.
import { plainAxiosInstance } from '#/axios';
const characterModule = {
namespaced: true,
state: {
characters: []
},
mutations: {
SET_CHARACTERS(state, characters) {
state.characters = characters;
}
},
actions: {
async fetchCharacters({ commit }) {
await plainAxiosInstance
.get('/characters')
.then(response => {
let characters = response.data;
commit('SET_CHARACTERS', characters);
console.log(characters); <-- Logs the expected response data
return characters;
})
.catch(error => console.log('Failed to fetch characters', error));
}
},
getters: {}
};
export default characterModule;
I am then dispatching the Vuex action inside of a Vue component on mount:
<script>
import { mapState, mapActions } from 'vuex';
export default {
mounted() {
this.fetchCharacters()
.then(response => {
console.log('response', response);
// response is logging undefined here <----
})
.catch(error => {
console.log(error);
});
},
computed: mapState(['character']),
methods: mapActions('character', ['fetchCharacters'])
};
</script>
The console.log in modules/character.js logs the data as expected and then the response inside of the component logs undefined. I made sure to return the variable characters in the Vuex module. And I also made the Vuex action fetchCharacters async. So why is the response in the component returning undefined?
Thanks if you can help.
Change this:
async fetchCharacters({ commit }) {
await plainAxiosInstance
to this:
fetchCharacters({ commit }) {
return plainAxiosInstance
You can keep the async if you want but it won't make any difference.
In its present form the action will implicitly return a promise and that promise won't resolve until the request is complete. However there's nothing to tell it to resolve that promise to the desired value.
Instead of waiting for the promise inside the action you can just return that promise instead. Externally that won't make any difference as you'll just get back a promise either way but crucially that promise will resolve to the correct value once the request is complete.
I'm new to react / redux
When I doing the project in jquery
I will make some functions like this:
errorHandle (code) {
if(code = 1){
$('.popUpA').show()
}
...
}
callAPI (){
//Do AJAX call
//If Error , call errorHandle()
}
In the new project,
I use the axios to call API in the api helper
export function getDataList(){
//axios....
}
export function getData(){
//axios....
}
And I use the Store to trigger the show/hide popUp , I will use dispatch(showPopup()) and dispatch(showPopup(hide)) in component
But I want that if api function have error , will pass the response in to the errorHandler , then dispatch the showPopup. I don't have idea how to add this into the exported function.
Any suggestion or example?
this is my abstraction of axios request, I use it in services that are used with Redux:
import axios from 'axios';
import { API } from '../../constants';
import { revokeAuthAction } from ;
export const getAuth = () => {
// Auth logic
};
/**
* Create an Axios Client with defaults
*/
const client = axios.create({
baseURL: API.BASEURL,
headers: {
Authorization: getAuth(),
'Access-Control-Max-Age': 1728000,
// 'X-Authorization-JWT':
},
});
/**
* Request Wrapper with default success/error actions
*/
const request = (options) => {
const onSuccess = (response) => options.raw ? response : response.data;
// console.debug('Request Successful!', response);
// If options.raw is true, return all response
const onError = (error) => {
// console.error('Request Failed:', error.config);
if (error.response) {
if (error.response.status === 401) {
// console.error('Unauthorized');
store.dispatch(revokeAuthAction());
} else {
// Request was made but server responded with something
// other than 2xx
// console.error('Status:', error.response.status);
// console.error('Data:', error.response.data);
// console.error('Headers:', error.response.headers);
}
} else {
// Something else happened while setting up the request
// triggered the error
// console.error('Error Message:', error.message);
}
return Promise.reject(error.response || error.message);
};
return client(options)
.then(onSuccess)
.catch(onError); // in realtà non catcho un bel niente perchè ritorno Promise.reject e quindi il giro ricomincia
};
export default request;
There are more library to handle redux async action calls. I use redux-thunk another well known library is redux-saga. With redux thunk you will add a middleware to redux and this way you can create async action creators which return a function, and they can call other action creators depending of the result of the async call.
You add the middleware this way:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
And your action creator will be something like this:
export function requestDataList() {
return function (dispatch: Function, getState: Function) {
return getDataList().then(resp => {
dispatch(dataListReceived(resp.data));
}).catch(error => {
dispatch(showPopup(title, error.message));
});
};
}
So if your getDataList retrun an axios promise, on success it will call an action wit the result. On error it can call the error dialog.
I'm using axios to do a post in my action creator. My action creator takes in an array of objects and posts it to an express server.
I'm trying to set the payload to be the response of the server but I realize that the response occurs after the action has been created.
export function sendOrders (data : Order[] ){
console.log("gets inside sendOrders action creator")
var output : string;
axios.post('http://localhost:8081/', data)
.then(function (response) {
output = response.data;
console.log(output)
})
.catch(function (error) {
output = error;
console.log(output)
});
return {
type: SEND_ORDERS,
payload : output
}
}
As a result, my reducer returns "undefined". Does anyone know how I can work around this?
Action creators are synchronous. That being said, there are redux plugins, like redux-thunk and redux-saga that allow an action creator to be asynchronous, which would let you emit an action after the Promise completes.
Example using redux-thunk:
export function sendOrders (data : Order[]) {
return (dispatch) => {
var output : string;
return axios.post('http://localhost:8081/', data)
.then(function (response) {
output = response.data;
console.log(output)
dispatch({
type: SEND_ORDERS,
payload: output
});
})
.catch(function (error) {
output = error;
console.log(output)
});
};
}
So essentially, your action creator returns a Promise-returning function which is passed dispatch and getState so you can asynchronously read state and dispatch actions. In order to use this example, you have to add the plugin. From the sample in the documentation:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);