Context
The URL in this app is only accessible in production and it is not able to access via local. When doing unit tests, I need to mock the response of that url.
What I got
Follow this tutorial
Code I have
saga.js
import {all, call, put, takeEvery} from 'redux-saga/effects';
import axios from 'axios';
async function myfetch(endpoint) {
const out = await axios.get(endpoint);
return out.data;
}
function* getItems() {
//const endpoint = 'https://jsonplaceholder.typicode.com/todos/1';
const endpoint = 'http://sdfds';
const response = yield call(myfetch, endpoint);
const items = response;
//test
console.log('items', items);
yield put({type: 'ITEMS_GET_SUCCESS', items: items});
}
export function* getItemsSaga() {
yield takeEvery('ITEMS_GET', getItems);
}
export default function* rootSaga() {
yield all([getItemsSaga()]);
}
You can see that I made endpoint as const endpoint = 'http://sdfds';, which is not accessible.
saga.test.js
// Follow this tutorial: https://medium.com/#lucaspenzeymoog/mocking-api-requests-with-jest-452ca2a8c7d7
import SagaTester from 'redux-saga-tester';
import mockAxios from 'axios';
import reducer from '../reducer';
import {getItemsSaga} from '../saga';
const initialState = {
reducer: {
loading: true,
items: []
}
};
const options = {onError: console.error.bind(console)};
describe('Saga', () => {
beforeEach(() => {
mockAxios.get.mockImplementationOnce(() => Promise.resolve({key: 'val'}));
});
afterEach(() => {
jest.clearAllMocks();
});
it('Showcases the tester API', async () => {
const sagaTester = new SagaTester({
initialState,
reducers: {reducer: reducer},
middlewares: [],
options
});
sagaTester.start(getItemsSaga);
sagaTester.dispatch({type: 'ITEMS_GET'});
await sagaTester.waitFor('ITEMS_GET_SUCCESS');
expect(sagaTester.getState()).toEqual({key: 'val'});
});
});
axios.js
const axios = {
get: jest.fn(() => Promise.resolve({data: {}}))
};
export default axios;
I was hopping this will overwrite the default axios
Full code here
Summary
Need to overwrite default axios' return response.
In order to replace a module, you need to save your mock module in a specific folder structure: https://jestjs.io/docs/en/manual-mocks#mocking-node-modules
So it seems that in your project - https://github.com/kenpeter/test-saga/blob/master-fix/src/tests/saga.test.js - you need to move the __mocks__ folder the root, where package.json is. Then import axios normally like import mockAxios from 'axios', and jest should take care of replacing the module.
Related
All I wanna do is be able to call logic from my geolocationApi file into my react-native components whenever I want, NOT LIKE A HOOK but normal async functions, I'm using a custom hook in the geolocationApi file I'm importing though! (custom hooks handles mobx state updates)
I want to call it like this in my functional components (plain and easy):
import geolocationApi from '#utils/geolocationApi.js'
const getCoords = async () =>
{
let result = await geolocationApi().requestLocationPermissions(true);
};
My geolocationApi file where I have a bunch of functions about geolocation I don't want to crowd my components with.
#utils/geolocationApi.js
import _ from 'lodash';
import Geolocation from 'react-native-geolocation-service';
import { useStore } from '#hooks/use-store';
const geolocationApi = () => {
//Custom hook that handles mobx stores
const root = useStore();
const requestLocationPermissions = async (getCityName = false) =>
{
const auth = await Geolocation.requestAuthorization("whenInUse");
if(auth === "granted")
{
root.mapStore.setLocationEnabled(true);
let coords = await getMe(getCityName);
return coords;
}
else
{
root.mapStore.setLocationEnabled(false);
}
};
const getMe = async () =>
{
Geolocation.getCurrentPosition(
async (position) => {
let results = await onSuccess(position.coords);
return results;
},
(error) => {
console.log(error.code, error.message);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
};
/*const onSuccess = async () => {}*/
};
export default geolocationApi;
This can't be that hard!
If I remove export default geolocationApi and instead add export const geolocationApi at the top I get:
geolocationApi.default.requestLocationPermissions is not a function
You cannot use hooks outside React components. You can pass down the state to your function
import geolocationApi from '#utils/geolocationApi.js'
const getCoords = async (root) =>
{
let result = await geolocationApi(root).requestLocationPermissions(true);
};
Then instead of using useStore()
import _ from 'lodash';
import Geolocation from 'react-native-geolocation-service';
import { useStore } from '#hooks/use-store';
// pass the root from top
const geolocationApi = (root) => {
// your logic
return {
requestLocationPermissions,
getMe
}
}
Then somewhere in your component tree, ( an example with useEffect )
import getCoords from 'path'
const MyComp = () => {
const root = useStore();
useEffect(() => {
getCoords(root)
}, [root])
}
As you said, geolocationApi is a regular function, not a React component/hook. So, it isn't inside the React lifecycle to handle hooks inside of it.
You can use the Dependency Injection concept to fix it.
Make geolocationApi clearly dependent on your store.
const geolocationApi = (store) => {
Then you pass the store instance to it.
const getCoords = async (store) =>
{
let result = await geolocationApi(store).requestLocationPermissions(true);
};
Whoever React component calls the getCoords can pass the store to it.
//...
const root = useStore();
getCoords(root);
//...
I'm using redux-thunk for async actions, and all was working as expected until I added an Apollo Client into the mix, and I can't figure out why. The action is being dispatched, but the return function is not being called.
-- Client provider so that I can use the client in the Redux store outside of the components wrapped in <ApolloProvider>.
import { ApolloClient, InMemoryCache, createHttpLink } from "#apollo/client";
class ApolloClientProvider {
constructor() {
this.client = new ApolloClient({
link: createHttpLink({ uri: process.env.gqlEndpoint }),
cache: new InMemoryCache(),
});
}
}
export default new ApolloClientProvider();
-- My store setup
const client = ApolloClientProvider.client;
const persistConfig = {
key: "root",
storage: storage,
};
const pReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(
pReducer,
applyMiddleware(thunk.withExtraArgument(client))
);
-- The action
export const fetchMakeCache = (token) => (dispatch, client) => {
console.log("fetching");
const query = gql`
query Source {
sources {
UID
Name
ActiveRevDestination
}
workspaces {
UID
Name
SourceUids
ActiveRevSource
}
}
`;
return () => {
console.log("reached return");
dispatch(requestMakeCache());
client
.query({
query: query,
context: {
headers: {
Authorization: `Bearer ${token}`,
},
},
})
.then((r) => r.json())
.then((data) => dispatch(receivedMakeCache(data)))
.catch((error) => dispatch(failedMakeCache(error)));
};
};
-- The component dispatching the thunk
import React from "react";
import { useAuth0 } from "#auth0/auth0-react";
import { useDispatch } from "react-redux";
import * as actions from "../store/actions";
const Fetch = () => {
const dispatch = useDispatch();
const { getAccessTokenSilently, isAuthenticated } = useAuth0();
if (isAuthenticated) {
// This will set loading = false immediately
// The async function below results in loading not being set to false
// until other components are performing actions that will error
dispatch(actions.requestMakeCache());
(async () => {
const token = await getAccessTokenSilently({
audience: process.env.audience,
});
dispatch(actions.fetchMakeCache(await token));
})();
}
return <></>;
};
export default Fetch;
When the Fetch component loads, the "fetching" console log prints so it's definitely being dispatched. But the "reached return" never gets hit. This exact same code worked as expected when not using the Apollo client. However, I've been able to use the same client successfully in a component. I'm not getting any errors, the return function just isn't being hit.
Most of the questions on here about thunks not running the return function have to do with not dispatching correctly, but I don't think that's the case since this worked pre-Apollo. (Yes, I know that using redux and Apollo together isn't ideal, but this is what I have to do right now)
I am using axios in my create-react-app. Which is the best way to use axios:
Method 1:
ajax.js
import axios from 'axios';
const axiosInstance = axios.create({});
export default axiosInstance;
app.js
import ajax from './ajax.js';
ajax.post('url');
Method 2:
ajax.js
import axios from 'axios';
class AjaxService{
constructor(apiConfig){
this.service = axios.create(apiConfig);
}
doGet(config){
return this.service.get(config.url);
}
...
}
export default AjaxService;
app.js:
import AjaxService from './ajax';
const service1 = new AjaxService();
service.doGet({url:'url'});
app2.js
import AjaxService from './ajax';
const service2 = new AjaxService();
service.doGet({url:'url'});
In method 2, we have to initialize the service wherever we make a call, which may or may not be a best practice. If we follow method 2, Is there a way to make it as a common service across the application?
i've seen a way in here and i came up with another solution like i explained below:
1 - i created my service with axios
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.REACT_APP_BASE_URL
// headers: { 'X-Custom-Header': 'foobar' }
});
// Add a request interceptor
instance.interceptors.request.use(
(config) => {
// Do something before request is sent
return config;
},
(error) => {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
instance.interceptors.response.use(
(response) => {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
},
(error) => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
}
);
export default instance;
2- i use that service to create a function for api call.
in here i can add a new AbortController for later use in useEffect.
import axios from 'services/request';
export function getMarket(type, callBack) {
const controller = new AbortController();
axios
.get(`https://dev.zh1.app/api/market/map?type=${type}`, {
signal: controller.signal
})
.then((res) => {
callBack(true, res.data);
})
.catch((res) => {
callBack(false, res.response);
});
return controller;
}
export default {
getMarket
};
3- in the hooks folder i created a hook called useApi. the controller from step 2 used in here. if you check the link above you can see the author add request function because you may have some props to pass to api call. i think it is valid but ugly. so i decided to create a closure for useApi to pass any params i want to the Axios in step 2.
import { useEffect, useState } from 'react';
// in useStrict mode useEffect call twice but will not in production
export default function useApi(apiFunc) {
return function useApiCall(...params) {
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const apiCall = useCallback(() => {
setLoading(true);
// controller is new AbortController which set each api function
const controller = apiFunc(...params, (ok, data) => {
setLoading(false);
if (ok) {
setResult(data);
} else {
setError(data.message);
}
});
return () => {
controller.abort();
};
}, []);
useEffect(() => {
apiCall();
}, []);
return {result, loading, error, [apiFunc.name]: apiCall};
};
}
4- finally in my react component
import { IconButton } from '#mui/material';
import useApi from '#share/hooks/useApi';
import { Refresh } from '#share/icons';
import { getCaptcha as CaptchaApi } from 'api/oauth/captcha';
import CaptchaStyle from './style';
export default function Captcha() {
const { result: captcha, getCaptcha } = useApi(CaptchaApi)();
return (
<CaptchaStyle>
<img src={`data:image/png;base64,${captcha?.base64}`} alt="captcha" />
<IconButton onClick={getCaptcha}>
<Refresh />
</IconButton>
</CaptchaStyle>
);
}
i think this approach i quite good and if you dont need to pass any props just call useApi([yourfunction])() with empty function.
and you can have access to the function inside of useApi if you need to call it again.
It totally depends on your project. If your project relies more on the function component then go ahead and use the first approach.
If you use classes for the majority of your components go for the second approach.
I generally use the first approach, it's easy and avoids this altogether. Also, it's easy to target multiple instances.
// Ajax.js file
import axios from "axios";
export function updateData=(body,callback){
le url= 'your api to call'
axios
.put(url, body)
.then((response) => response.data)
.then((res) => {
callback(res);
})
.catch((error) => {
console.log(error);
callback('error occurred');
});
}
// app.js file
import {updateData} from './ajax.js'
//Place your code where you need
updateData(yourBodyToPass,res=>{
//Stuff after the response
)
Note:- pass your data as first argument and get response of api from second
I am using vuex, axios for my app and I want to use getter in axios initiation to pass basic auth. This is my axios init (http-common.js):
import axios from 'axios'
import store from '#/store'
export default axios.create({
baseURL: 'http://localhost:8081/',
auth: store.getters['authentification']
})
When I am debugging my app I find store undefined. Can someone explain what am I doing wrong? Store itself works fine in all the components.
My store has several modules and those modules. store index.js:
import m1 from './modules/m1'
import m2 from './modules/m2'
import authentification from './modules/authentification'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
authentification,
m1,
m2
}
})
And modules uses axios init function for calling REST api i.e. :
import HTTP from '#/common/http-common'
.....
const actions = {
action ({commit}) {
HTTP.get('item')
.then(response => {
commit('MUTATION', {response})
})
}
}
.....
export default {
state,
getters,
actions,
mutations
}
I think this creates loop and calls http-common before store is being initialized.
Edit: adding authentification module as requested:
import * as types from '../mutation-types'
const state = {
isLoggedIn: !!localStorage.getItem('auth'),
auth: JSON.parse(localStorage.getItem('auth'))
}
const getters = {
isLoggedIn: state => {
return state.isLoggedIn
},
authentification: state => {
return state.auth
}
}
const mutations = {
[types.LOGIN] (state) {
state.pending = true
},
[types.LOGIN_SUCCESS] (state) {
state.isLoggedIn = true
state.pending = false
},
[types.LOGOUT] (state) {
state.isLoggedIn = false
}
}
const actions = {
login ({
state,
commit,
rootState
}, creds) {
console.log('login...', creds)
commit(types.LOGIN) // show spinner
return new Promise(resolve => {
setTimeout(() => {
localStorage.setItem('auth', JSON.stringify(creds))
commit(types.LOGIN_SUCCESS)
resolve()
}, 1000)
})
},
logout ({ commit }) {
localStorage.removeItem('auth')
commit(types.LOGOUT)
}
}
export default {
state,
getters,
actions,
mutations
}
This is actually a better solution shown to me by Thorsten Lünborg (LinusBorg) of the Vue core team:
https://codesandbox.io/s/vn8llq9437
In the file that you define your Axios instance in and set configuration, etc., you have also got a Vuex plugin that watches your store and sets/deletes your Authorization header based on the presence of whatever auth token in your store.
I have found a sollution. I had to assign auth before the call and not during inicialization of axios object:
var axiosInstance = axios.create({
baseURL: 'http://localhost:8081/'
})
axiosInstance.interceptors.request.use(
config => {
config.auth = store.getters['authentification']
return config
}, error => Promise.reject(error))
export default axiosInstance
Trying to test my async action creators using this example: http://redux.js.org/docs/recipes/WritingTests.html#async-action-creators and I think I did everything the same but I've got an error:
async actions › creates FETCH_BALANCE_SUCCEESS when fetching balance has been done
TypeError: store.dispatch(...).then is not a function
Don't understand why it's happened because I did everything from the example step by step.
I also found this example http://arnaudbenard.com/redux-mock-store/ but anyway, mistake exists somewhere and unfortunately, I can't find it. Where is my mistake, why I've got an error even If my test case looks like the same as an example
My test case:
import nock from 'nock';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from './';
import * as types from '../constants';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('async actions', () => {
afterEach(() => {
nock.cleanAll();
});
it('creates FETCH_BALANCE_SUCCEESS when fetching balance has been done', () => {
const store = mockStore({});
const balance = {};
nock('http://localhost:8080')
.get('/api/getbalance')
.reply(200, { body: { balance } });
const expectedActions = [
{ type: types.FETCH_BALANCE_REQUEST },
{ type: types.FETCH_BALANCE_SUCCEESS, body: { balance } },
];
return store.dispatch(actions.fetchBalanceRequest()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
My actions which I am trying to test.
import 'whatwg-fetch';
import * as actions from './actions';
import * as types from '../constants';
export const fetchBalanceRequest = () => ({
type: types.FETCH_BALANCE_REQUEST,
});
export const fetchBalanceSucceess = balance => ({
type: types.FETCH_BALANCE_SUCCEESS,
balance,
});
export const fetchBalanceFail = error => ({
type: types.FETCH_BALANCE_FAIL,
error,
});
const API_ROOT = 'http://localhost:8080';
const callApi = url =>
fetch(url).then(response => {
if (!response.ok) {
return Promise.reject(response.statusText);
}
return response.json();
});
export const fetchBalance = () => {
return dispatch => {
dispatch(actions.fetchBalanceRequest());
return callApi(`${API_ROOT}/api/getbalance`)
.then(json => dispatch(actions.fetchBalanceSucceess(json)))
.catch(error =>
dispatch(actions.fetchBalanceFail(error.message || error))
);
};
};
In your test you have
return store.dispatch(actions.fetchBalanceRequest()).then(() => { ... })
You're trying to test fetchBalanceRequest, which returns an object, so you cannot call .then() on that. In your tests, you would actually want to test fetchBalance, since that is an async action creator (and that is what is explained in the redux docs you posted).
That's usually a problem with redux-mock-store
Remember that:
import configureStore from 'redux-mock-store'
The function configureStore does not return a valid store, but a factory.
Meaning that you have to call the factory to get the store:
const store = configureStore([])()