Component:
export const fetchList = () => {
return API.get(AMPLIFY_ENPOINTS.default, API_URLS.list, { response: true });
}
const List: React.FC = () => {
const dispatch = useDispatch();
const setError = useError();
useEffect(() => {
fetchList()
.then((response) => {
if (response && response.data?.length) {
dispatch(setList(response.data));
}
})
.catch((error) => {
setError(error);
});
}, [])
}
Test:
it('should fetch list', async () => {
const wrapper = mount(
<Provider store={store}>
<List />
</Provider>
);
API.get = jest.fn().mockImplementation(() => Promise.resolve({ data: mockList }));
const response = await fetchList();
console.log(store.getActions(), response); // HERE IS THE ISSUE
});
So the store.getActions() returns setError from catch block, why is that? It should return setList from then block. What am I doing wrong? response variable returns mockList just fine.
Edit
The error it returns is API not configured, I'm using aws amplify.
fetchList is called when the component is mounted, mocked API.get doesn't affect the first time it's called, and second call doesn't do anything. It's a bad practice to mock methods by assigning a spy to them because they cannot be restored after a test.
The problem with fetchList is that it cannot be spied or mocked because it's used in the same module it's defined. The promise it creates in useEffect cannot be chained, promises need to be flushed in order to avoid race condition.
It can be:
let flushPromises = () => new Promise(resolve => setImmediate(resolve));
jest.spyOn(API, 'get').mockResolvedValue({ data: mockList });
const wrapper = mount(
<Provider store={store}>
<List />
</Provider>
);
await flushPromises();
expect(store.getActions())...
Related
I'm trying to write a test (with react-testing-library) for a component that makes an api call inside a useEffect and then updates redux state. The component should then update with the new state, but in my test, the component never seems to get updates to redux state.
I've tried logging at each step of the process: I found that the redux state was updating as expected during the batched actions in apiCall(). Then when "otherReduxAction" is called, it seems to be acting on a stale store. Moving that action out of the component fixed the problem. Same with the subsequent "setIsFetching" call. However, while moving those actions out of the component helped to persist state changes, the component still never gets the state updates.
This behavior only exists in testing. The app updates as expected.
component:
const CallsList = () => {
const dispatch = useDispatch();
const { filteredCalls, isFetching } = useSelector(selectState);
useEffect(() => {
async function fetchRecordings() {
dispatch(setIsFetching(true));
try {
await apiCall();
dispatch(otherReduxAction());
} catch (err) {}
dispatch(setIsFetching(false));
}
fetchRecordings();
}, [dispatch]);
return (
isFetching ? (
RENDER PROGRESS BAR
) : filteredCalls.length ? (
RENDER LIST OF CALLS
) : (
RENDER NO RESULTS MESSAGE
)
);
};
apiCall:
export const apiCall = (): Promise<string> => {
return new Promise(async (res, rej) => {
try {
const response = await axios.get<GetResponse>(
`${process.env.REACT_APP_API_BASE_URL}/recordings`,
);
batch(() => {
store.dispatch(setCalls(response.data.recordings));
... DISPATCH ADDITIONAL ACTIONS
});
res("success");
} catch (err) {
rej("error");
}
});
};
msw handler:
rest.get(`${process.env.REACT_APP_API_BASE_URL}/recordings`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({ MOCKED RESPONSE OBJECT })
);
}),
custom react-testing-library render:
import { render as rtlRender } from "#testing-library/react";
...
export const render = (ui: any, initialStore = {}, { route = '/' } = {}, options = {}, ) => {
const store = createStore(rootReducer, initialStore);
const Providers = ({ children }: any) => (
<Provider store={store}>
<BrowserRouter>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</BrowserRouter>
</Provider>
);
window.history.pushState({}, 'Test page', route)
return rtlRender(ui, { wrapper: Providers, ...options });
};
test (this fails on awaiting the element with LIST ITEM TEXT:
describe("CallsList", () => {
it("renders list of recordings", async () => {
render(<CallsList />);
const progressBar = screen.getByRole("progressbar");
expect(progressBar).toBeInTheDocument();
await waitForElementToBeRemoved(circularProgress)
expect(await screen.findByText(LIST ITEM TEXT)).toBeInTheDocument();
});
});
I have a function in my React app's App component that refreshes the user's access token when it's first rendered (useEffect hook). At the moment, unit tests are checking to see how the state has changed at the end of the component's rendering. How can I make the function itself more testable?
I've considered refactoring to have the dispatch() hook, logout() reducer, and local setLoading() state function passed into the function as arguments so they can be mocked/so the function can be externalized from the component itself, but I'm not sure what value this would bring.
I understand that 100% test coverage is not necessary, but I'm learning and want to do the best I can while I do so.
A little context:
The app uses a ReduxToolkit slice for authentication state, including the user object and access token for the currently authenticated user, or nulls for guest users.
Auto refresh logic is implemented into a custom fetchBaseQuery.
The code below describes refreshing the access token for a user who's logged in and has a refresh token in localStorage, but has refreshed the page, clearing the redux state. It refreshes the accessToken before rendering any routes/views to avoid the user having to enter credentials every time the page refreshes.
Here's the current implementation:
//imports
...
const App = () => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
useEffect(() => {
const refresh = async () => {
const token = localStorage.getItem("refreshToken");
if (token) {
const refreshRequest = {
refresh: token,
};
const response = await fetch(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
}
);
if (response.status === 200) { // This branch gets no test coverage and I can't figure out how to fix that.
const data: RefreshResponse = await response.json();
// Should this be passed into the function to make it more reusable/testable?
dispatch(
setCredentials({ user: data.user, token: data.accessToken })
);
}
}
// Should this be passed into the function to make it more reusable/testable?
setLoading(false);
};
refresh();
}, [dispatch]);
if (loading) return (
<div className="h-100 d-flex justify-content-center align-items-center bg-dark">
<Spinner animation="border" />
</div>
);
return (
<>
<Routes>
// Routes
</Routes>
</>
);
}
export default App;
and here are the relevant test cases:
it("should successfully request refresh access token on render", async () => {
// refresh() expects a refreshToken item in localStorage
localStorage.setItem("refreshToken", "testRefreshToken");
// I can't use enzyme because I'm on react 18, so no shallow rendering afaik :/
// renderWithProviders renders including a redux store with auth/api reducers
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBe("testAccessToken");
});
localStorage.removeItem("refreshToken");
});
it("should fail to request refresh access token on render", async () => {
localStorage.setItem("refreshToken", "testRefreshToken");
// msn api route mocking, force a 401 error rather than the default HTTP 200 impl
server.use(
rest.post(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
(req, res, ctx) => {
return res(ctx.status(401));
}
)
);
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBeNull();
});
localStorage.removeItem("refreshToken");
});
it("should not successfully request refresh access token on render", async () => {
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBe(null);
});
});
My suggestions:
Move dispatch, useState and useEffect to custom hook. It can look like:
const useTockenRefresh() { // Name of the custom hook can be anything that is
started from work 'use'
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
useEffect(() => {
/* useEffect code as is */
}, [/* deps */])
return loading
}
export default useTockenRefresh
Utilize useTockenRefresh in your component
const App = () => {
const loading = useTockenRefresh()
if (loading) return (
// And rest of your code
}
Now it is possible to test only useTockenRefresh in isolation. I would suggest to use React Hooks Testing Library for this purpose. And as this will be Unit Tests, it is better to mock everything external, like useAppDispatch, fetch, etc.
import { renderHook, act } from '#testing-library/react-hooks'
// Path to useTockenRefresh should be correct relative to test file
// This mock mocks default export from useTockenRefresh
jest.mock('./useTockenRefresh', () => jest.fn())
// This mock for the case when useAppDispatch is exported as named export, like
// export const useAppDispatch = () => { ... }
jest.mock('./useAppDispatch', () => ({
useAppDispatch: jext.fn(),
}))
// If fetch is in external npm package
jest.mock('fetch', () => jest.fn())
jest.mock('./setCredentials', () => jest.fn())
// Mock other external actions/libraries here
it("should successfully request refresh access token on render", async () => {
// Mock dispatch. So we will not update real store, but see if dispatch has been called with right arguments
const dispatch = jest.fn()
useAppDispatch.mockReturnValueOnce(dispatch)
const json = jest.fn()
fetch.mockReturnValueOnce(new Promise(resolve => resolve({ status: 200, json, /* and other props */ })
json.mockReturnValueOnce(/* mock what json() should return */)
// Execute hook
await act(async () => {
const { rerender } = renderHook(() => useTockenRefresh())
return rerender()
})
// Check that mocked actions have been called
expect(fetch).toHaveBeenCalledWith(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
})
expect(setCredentials).toHaveBeenCalledWith(/* args of setCredentials from mocked responce object */
// And so on
}
My scenario is just one step ahead of this existing question in stackoverflow
I have dispatch fn but with .then() followed by.
Component:
const Testing = props => {
const [counterData, setCounterData] = useState(0)
const resData = useSelector(state => state.test)
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchTestinData())
.then(res => {
console.log(res);
setCounterData(prev => prev + 1)
})
}, [])
return <div>
Testing component
<div>
Result - {resData.title}
</div>
<div>
Counter - {counterData}
</div>
</div>
}
Test file:
// const mockDispatch = jest.fn().mockResolvedValueOnce({json : async () => []})
const mockDispatch = jest.fn().mockImplementation(() =>
Priomise.resolve({title:'tets'}))
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch
}))
describe('<Testing />', function () {
const getComponent = (state) => <Provider store={store}>
<Testing />
</Provider>
it('testing success api', () => {
// window.fetch = jest.fn().mockResolvedValueOnce({title: 'testing title'})
render(getComponent())
screen.debug()
// expect(mockDispatch).toBeCalledTimes(1)
})
})
if am using just jest.fn() getting same error as well as with mock implemntaion.
Error screenshot
Something am missing in mock fn implementaion.
Plase help. Searched a lot but no luck.
Apparently Jest docs are a bit misleading about the possibility to use previously defined variables in a mock module factory: that is just not possible.
So the solution to your issue is just to move your mockDispatch implementation inside the module factory:
jest.mock('react-redux',
() => ({
...jest.requireActual('react-redux'),
useDispatch: () => jest.fn().mockImplementation(() =>
Promise.resolve({ title: 'test' }))
})
)
I have a component the uses useEffect to fetch data from a file.
In the component i have a condiiton that only shows the content of the component if we have data.
Now how can a test the conditional part of the content i my test case?
This is what i have right now:
Component:
function MunicipalityInfo() {
const [municipalityData, setMunicipalityData] = useState({})
const fetchData = async () => {
try{
const result = await fetch(XMLFile)
const data = await result.text();
const xml = new XMLParser().parseFromString(data);
const res = XMLMapper(xml)
setMunicipalityData(res)
}catch(e){
console.log(e)
}
}
useEffect(() => {
fetchData();
}, []);
return(
<>
{ municipalityData.units &&
municipalityData.units.map((city, index) => {
return (
<Div key={index} data-testid="municipalityInfo-component" className="mt-5 p-3">
<HeaderMain data-testid="header-main">{city.City}</HeaderMain>
<HeaderSub data-testid="header-sub" className="mt-4">{city.venamn}</HeaderSub>
<BodyText data-testid="body-text">{city.Address}, {city.City}</BodyText>
<MapLink href={"#"} data-testid="map-link"><i data-testid="map-icon" className="fas fa-map-marker-alt"></i> Show on map</MapLink>
<LinkList data-testid="link-list">
<LinkListItem data-testid="list-item-first">
<Link href={city.BookingURL} data-testid="link-book-vaccination">Some text</Link>
</LinkListItem>
</LinkList>
<Calendar data={city.unit}/>
</Div>
)
})
}
<CitiesSideBar>
<Sidebar data={municipalityData.cities}/>
</CitiesSideBar>
</>
)
}
export default MunicipalityInfo;
And this is my test:
describe("<MunicipalityInfo />", () => {
it("renders without crashing", async () => {
const {queryByTestId, findByText, findByTestId} = render(<MunicipalityInfo/>, {})
expect(queryByTestId("municipalityInfo-component")).not.toBeInTheDocument();
expect(await findByTestId("municipalityInfo-component")).toBeInTheDocument(); <--- this line fails
})
})
And the error of my testcase:
TestingLibraryElementError: Unable to find an element by: [data-testid="municipalityInfo-component"]
if your problem is trying to test if something shouldn't be in the page...
use the queryBy
if you're want it to wait for something... then you want to await findBy (or wrap in a waitFor)
here's the docs: https://testing-library.com/docs/react-testing-library/cheatsheet/
I'm assuming you're mocking the fetch request so it wouldn't be the test problem...
if you're not mocking it... then you probably should mock and return either data or no data to test if it should or not render.
one way to elegantly "avoid" mocking would be by abstracting it in a custom hook:
function useCustomHook(){
const [municipalityData, setMunicipalityData] = useState({})
useEffect(() => {
fetch(XMLData)
.then((res) => res.text())
.then(async (data) => {
let xml = new XMLParser().parseFromString(data);
let result = await XMLMapper(xml)
setMunicipalityData(await result)
})
.catch((err) => console.log(err));
}, []);
return municipalityData;
}
function MunicipalityInfo({municipalityData = useCustomHook()}) { ... }
then in the test you can simply
render(<MunicipalityInfo municipalityData={'either null or some mocked data'} />)
Summary
I'm writing test code for my react app, but somehow, it always fails.
My app code is very simple, there is only one button, and if it's clicked, a function handleSubmit is fired.
What the handler does are
Fetching data from backend(This is async function)
Move to /complete page.
What I did
I mocked the function fetching data from API in test code
I mocked the useHistory in test code
Note
I realized that if the line that is fetching data from API is commented out, the test will pass.
Code
My main app code
import { useFetchDataFromAPI } from '#/usecase/useFetchDataFromAPI';
:
const { fetchDataFromAPI } = useFetchDataFromAPI();
:
const handleSubmit = async () => {
// If the line below is not commented out, test will fail
// const { id } = await fetchDataFromAPI();
history.push(`/complete`);
};
return (
<>
<button onClick={handleSubmit}>Button</button>
</>
My test code
:
jest.mock('#/usecase/useFetchDataFromAPI', () => ({
useFetchDataFromAPI: () => {
return { fetchDataFromAPI: jest.fn((): number => {
return 1;
})}
}
}));
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom') as any,
useHistory: () => ({
push: mockHistoryPush,
}),
}));
:
const renderApplicationWithRouterHistory = () => {
const history = createMemoryHistory();
const wrapper = render(
<Router history={history}>
<Application />
</Router>
);
return { ...wrapper, history };
};
:
describe('Test onClick handler', async () => {
test('Submit', () => {
const { getByText, getByRole } = renderApplication();
const elementSubmit = getByText('Button');
expect(elementSubmit).toBeInTheDocument();
fireEvent.click(elementSubmit);
expect(mockHistoryPush).toHaveBeenCalled();
});
});
Your event handler is called on button click, but because it is asynchronous, its result is not evaluated until after your test runs. In this particular case, you don't need the async behavior, so just use:
const handleSubmit = () => {
history.push(`/complete`)
}
testing-library provides a method waitFor for this if your handler did need to await something:
await waitFor(() => expect(mockHistoryPush).toHaveBeenCalled())
Though another simple way is to simply await a promise in your test so that the expectation is delayed by a tick:
fireEvent.click(elementSubmit);
await Promise.resolve();
expect(mockHistoryPush).toHaveBeenCalled();