I want to test a custom hook which was implemented as an helping function for code reuse with other hooks. It's calling useDispatch and useSelector in its implementation, along with saving data in the session storage:
export function useCustomHook(key, obj)
{
let myObject = {
somefield: obj.someField
};
sessionStorage.setItem(key, JSON.stringify(myObject));
const dispatch = useDispatch();
dispatch(actionCreator.addAction(key, myObject));
}
And the test:
it('should have data in redux and session storage', () =>
{
const obj = {
somefield: 'my val',
};
renderHook(() => useCustomHook('some key', obj));
let savedObj= JSON.parse(sessionStorage.getItem('some key'));
expect(savedObj.someField).toBe('my val');
renderHook(() =>
{
console.log("INSIDE");
let reduxObj = useSelector(state => state.vals);
console.log("THE OBJECT: " );
console.log(reduxObj);
expect(reduxObj).toBe(2); //just to see if it fails the test - it's not
});
})
No matter what I try, the test only arrives to the "INSIDE" console log and does not print the "THE OBJECT: " console log. The test itself still passes so it's like the useSelector somehow stops the rest of the renderHook execution.
I'm guessing it's related to the fact that the test doesn't have a store connected... What can be done to test redux in this case?
You'd need to provide a wrapper component that add the redux Provider with a store to connect to:
it('should have data in redux and session storage', () =>
{
const obj = {
somefield: 'my val',
};
const store = {} // create/mock a store
const wrapper = ({ children }) => <Provider store={store}>{children}</Provider>
renderHook(() => useCustomHook('some key', obj), { wrapper });
let savedObj= JSON.parse(sessionStorage.getItem('some key'));
expect(savedObj.someField).toBe('my val');
renderHook(() => {
console.log("INSIDE");
let reduxObj = useSelector(state => state.vals);
console.log("THE OBJECT: " );
console.log(reduxObj);
expect(reduxObj).toBe(2); //just to see if it fails the test - it's not
}, { wrapper });
})
Just as a side note, the renderHook is catching the errors, which is why you aren't seeing them in your test, if you had tried to access result.current it would have thrown, and you could have seen it represented in result.error, but the usage here of not returning a value from a custom hook to be asserted against is quite unusual.
This will likely also cause you issues by having the assertion inside second renderHook call. You'll probably want to either return the value from the hook and assert outside, or assert the updated value in the redux store instead.
Related
I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.
I am having trouble with accessing the data after fetching it with SecureStore in Expo for react-native.
Here is the simple code:
const infoofuser = SecureStore.getItemAsync('userInfo').then(value =>
console.log(`this is the vlaue from infouser: ${value}`),
);
console.log(`infoouser: ${JSON.stringify(infoofuser)}`);
the first infoofuser constant definition returns the object of the intended data.
console.log(`infoouser: ${JSON.stringify(infoofuser)}`);
however returns {"_U":0,"_V":0,"_W":null,"_X":null} which U understand is a promise. I would like to simply get the data that comes from the SecureStore call and use it to set my initialState in redux.
const infoofuser = SecureStore.getItemAsync('userInfo').then(value =>
value
);
this does not work either to access the data
You can use async method using async/await. Try this:
const userInfo = useSelector(state => state.userInfo);
const getData = async () => {
try {
const infoofuser = await SecureStore.getItemAsync('userInfo');
console.log('infoofuser:', infoofuser)
/// strore on redux
} catch (err) {
// handle error
}
}
useEffect(() => {
getData()
}, [])
if (!userInfo) return null
//render something else
You can check the Expo Secure Store docs for reference.
I am trying to write the Jest-enzyme test case for useEffect react hooks, and I am really lost, I want to write test case for 2 react hooks, one making the async call and another sorting the data and setting the data using usestate hooks, my file is here.
export const DatasetTable: React.FC<DatasetTableProps> = ({id, dataset, setDataset, datasetError, setDataSetError}) => {
const [sortedDataset, setSortedDataset] = useState<Dataset[]>();
useEffect(() => {
fetchRegistryFunction({
route:`/dataset/report?${constructQueryParams({id})}`,
setData: setDataset,
setError: setDataSetError
})();
}, [id, setDataset, setDataSetError]});
useEffect(() => {
if(dataset) {
const sortedDatasetVal = [...dataset];
sortedDatasetVal.sort(a, b) => {
const dateA: any = new Date(a.date);
const dateA: any = new Date(a.date);
return dataA - dateB;
}
setSortedDataset(sortedDatasetVal);
}
}, [dataset])
return (
<div>
<DatasetTable
origin="Datasets"
tableData={sortedDataset}
displayColumns={datasetColumns}
errorMessage={datasetError}
/>
</div>
);
}
Enzyme isn't the right library for this kind of testing.
https://react-hooks-testing-library.com/ is what you need.
In your case I would extract all the data fetching to a 'custom hook' and then test this independently from your UI presentation layer.
In doing so you have better separation of concerns and your custom hook can be used in other similar react components.
I managed to get enzyme to work with a data fetching useEffect hook. It does however require that you allow your dataFetching functions to be passed as props to the component.
Here's how I would go about testing your component, considering it now accepts fetchRegistryFunction as a prop:
const someDataSet = DataSet[] // mock your response object here.
describe('DatasetTable', () => {
let fetchRegistryFunction;
let wrapper;
beforeEach(async () => {
fetchRegistryFunction = jest.fn()
.mockImplementation(() => Promise.resolve(someDataSet));
await act(async () => {
wrapper = mount(
<DatasetTable
fetchRegistryFunction={fetchRegistryFunction}
// ... other props here
/>,
);
});
// The wrapper.update call changes everything,
// act seems to not automatically update the wrapper,
// which lets you validate your old rendered state
// before updating it.
wrapper.update();
});
afterEach(() => {
wrapper.unmount();
jest.restoreAllMocks();
});
it('should display fetched data', () => {
expect(wrapper.find(DatasetTable).props().tableData)
.toEqual(someDataSet);
});
});
Hope this helps!
This question is regarding: how to test a component which uses the useEffect hook and the useState hook, using the react-testing-library.
I have the following component:
function MyComponent() {
const [state, setState] = useState(0);
useEffect(() => {
// 'request' is an async call which takes ~2seconds to complete
request().then(() => {
setState(1);
});
}, [state]);
return <div>{state}</div>
}
When I render this application, the behavior I see is as follows:
The app initially renders 0
After ~2seconds, the app renders 1
Now, I want to test and assert this behavior using the react-testing-library and jest.
This is what I have so far:
import {render, act} from '#testing-library/react';
// Ignoring the describe wrapper for the simplicity
test('MyComponent', async () => {
let result;
await act(async () => {
result = render(<MyComponent />);
});
expect(result.container.firstChild.textContent).toBe(1);
})
The test passes. However, I also want to assert the fact that the user initially sees the app rendering 0 (before it renders 1 after 2 seconds).
How do I do that?
Thanks in advance.
As pointed out by Sunil Pai in this blog: https://github.com/threepointone/react-act-examples/blob/master/sync.md
Here's how I managed to solve this:
import {request} from '../request';
jest.mock('../request');
test('MyComponent', async () => {
let resolve;
request.mockImplementation(() => new Promise(resolve => {
// Save the resolver to a local variable
// so that we can trigger the resolve action later
resolve = _resolve;
}));
let result;
await act(async () => {
result = render(<MyComponent />);
});
// Unlike the non-mocked example in the question, we see '0' as the result
// This is because the value is not resolved yet
expect(result.container.firstChild.textContent).toBe('0');
// Now the setState will be called inside the useEffect hook
await act(async () => resolve());
// So now, the rendered value will be 1
expect(result.container.firstChild.textContent).toBe('1');
})
I am trying to test a react functional component using hooks. The useEffect hook makes a call to a third part API which then calls setState on return.
I have the test working but keep getting a warning that an update to the component was not wrapped in act.
The problem I have is that the expectation is inside a moxios.wait promise and therefore I cannot wrap that in an act function and then assert on the result of that.
The test passes but I know not wrapping code that updates state in an act function could lead to false positives or uncovered bugs. I'm just wondering how I should be testing this.
I've tried using the new async await act function in the react 16.9.0 alpha release as well as numerous suggestions I've found in many github issues like jest setTimers and none seem to solve the issue.
The component
const Benefits = props => {
const [benefits, setBenefits] = useState([])
const [editing, setEditing] = useState(false)
const [editingBenefit, setEditingBenefit] = useState({id: null, name: '', category: ''})
useEffect(() => {
axios.get('#someurl')
.then(response => {
setBenefits(response.data)
})
}, [])
}
The test
describe('Benefits', () => {
it('fetches the list of benefits from an api and populates the benefits table', (done) => {
const { rerender } = render(<Benefits />)
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response: benefits
}).then(() => {
expect(document.querySelectorAll('tbody > tr').length).toBe(2)
done()
})
})
})
})
The test passes but I get the following warning
Warning: An update to Benefits inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in Benefits (at benefits.spec.js:28)
from react 16.9.0 you can use async/await act
Your code should look like this
describe('Benefits', () => {
it('fetches the list of benefits from an api and populates the benefits table', async() => {
const { rerender } = render(<Benefits />);
await moxios.wait(jest.fn);
await act(async() => {
const request = moxios.requests.mostRecent()
await request.respondWith({
status: 200,
response: benefits
});
});
expect(document.querySelectorAll('tbody > tr').length).toBe(2)
})
I use jest.fn in moxios.wait because it needs callback function