How to test components using new react router hooks?
React RouterReact Router Problem Overview
Until now, in unit tests, react router match params were retrieved as props of component. So testing a component considering some specific match, with specific url parameters, was easy : we just had to precise router match's props as we want when rendering the component in test (I'm using enzyme library for this purpose).
I really enjoy new hooks for retrieving routing stuff, but I didn't find examples about how to simulate a react router match in unit testing, with new react router hooks ?
React Router Solutions
Solution 1 - React Router
Edit: The proper way of doing this the way described in Catalina Astengo's answer as it uses the real router functionality with just the history/routing state mocked rather than mocking the entire hook.
The way I ended up solving it was by mocking the hooks in my tests using jest.mock:
// TeamPage.test.js
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
useParams: () => ({
companyId: 'company-id1',
teamId: 'team-id1',
}),
useRouteMatch: () => ({ url: '/company/company-id1/team/team-id1' }),
}));
I use jest.requireActual
to use the real parts of react-router-dom for everything except the hooks I'm interested in mocking.
Solution 2 - React Router
I looked at the tests for hooks in the react-router
repo and it looks like you have to wrap your component inside a MemoryRouter
and Route
. I ended up doing something like this to make my tests work:
import {Route, MemoryRouter} from 'react-router-dom';
...
const renderWithRouter = ({children}) => (
render(
<MemoryRouter initialEntries={['blogs/1']}>
<Route path='blogs/:blogId'>
{children}
</Route>
</MemoryRouter>
)
)
Hope that helps!
Solution 3 - React Router
In your component use hooks as below
import {useLocation} from 'react-router';
const location = useLocation()
In your test spy on reactRouter Object as below
import routeData from 'react-router';
const mockLocation = {
pathname: '/welcome',
hash: '',
search: '',
state: ''
}
beforeEach(() => {
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation)
});
Solution 4 - React Router
If you're using react-testing-library
for testing, you can get this mock to work like so.
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({ state: { email: '[email protected]' } }),
}));
export const withReduxNRouter = (
ui,
{ store = createStore(rootReducer, {}) } = {},
{
route = '/',
history = createMemoryHistory({ initialEntries: [ route ] }),
} = {}
) => {
return {
...render(
<Provider store={store}>
<Router history={history}>{ui}</Router>
</Provider>
),
history,
store,
};
};
You should have mocked react-router-dom
before it has been used to render your component.
I'm exploring ways to make this reusable
Solution 5 - React Router
I am trying to get if the push
function in useHistory
is called by doing that but I can't get the mocked function calls...
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
fireEvent.click(getByRole('button'));
expect(mockHistoryPush).toHaveBeenCalledWith('/help');
It says that mockHistoryPush
is not called when the button has onClick={() => history.push('/help')}
Solution 6 - React Router
My use case was unit testing a custom hook using using useLocation(). I had to override the inner properties of useLocation which was read-only.
\\ foo.ts
export const useFoo = () => {
const {pathname} = useLocation();
\\ other logic
return ({
\\ returns whatever thing here
});
}
/*----------------------------------*/
\\ foo.test.ts
\\ other imports here
import * as ReactRouter from 'react-router';
Object.defineProperty(ReactRouter, 'useLocation', {
value: jest.fn(),
configurable: true,
writable: true,
});
describe("useFoo", () => {
it(' should do stgh that involves calling useLocation', () => {
const mockLocation = {
pathname: '/path',
state: {},
key: '',
search: '',
hash: ''
};
const useLocationSpy = jest.spyOn(ReactRouter, 'useLocation').mockReturnValue(mockLocation)
const {result} = renderHook(() => useFoo());
expect(useLocationSpy).toHaveBeenCalled();
});
});
Solution 7 - React Router
A slight variation of the above solutions which includes several params and query strings for a more complex scenario. This is easy to abstract into a utility function similar to a few above which can be reused by other tests.
short version
<MemoryRouter
initialEntries={[
'/operations/integrations/trello?business=freelance&businessId=1&pageId=1&pageName=Trello',
]}
>
<Route path="/operations/:operation/:location">
<OperationPage />
</Route>
</MemoryRouter>
Longer version:
The example snippets below include a full example of the test file, component and logs to help leave little room for interpretation.
includes:
- react 16
- redux 7
- react-router-dom 5
- typescript
- thunk
- sagas
- @testing-library/react 11
operations.spec.tsx
import React from 'react'
import { MemoryRouter, Route } from 'react-router-dom'
import { render, screen } from '@testing-library/react'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
import createDebounce from 'redux-debounced'
import thunk from 'redux-thunk'
import createSagaMiddleware from 'redux-saga'
import rootReducer from 'redux/reducers/rootReducer'
import OperationPage from '../operation'
import { initialState } from '../mock'
import '@testing-library/jest-dom' // can be moved to a single setup file
const sagaMiddleware = createSagaMiddleware()
const middlewares = [thunk, sagaMiddleware, createDebounce()]
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
rootReducer,
// any type only until all reducers are given a type
initialState as any,
composeEnhancers(applyMiddleware(...middlewares))
)
const Wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>
describe('Operation Page - Route', () => {
it('should load', async () => {
const Element = () => (
<MemoryRouter
initialEntries={[
'/operations/integrations/trello?business=freelance&businessId=1&pageId=1&pageName=Trello',
]}
>
<Route path="/operations/:operation/:location">
<OperationPage />
</Route>
</MemoryRouter>
)
render(<Element />, { wrapper: Wrapper })
// logs out the DOM for further testing
screen.debug()
})
})
logs and the component via operations.tsx
. Got lazy including the all types (via typescript) for this component but outside of scope :)
import React from 'react'
import { useParams, useLocation } from 'react-router-dom'
import { connect } from 'react-redux'
import queryString from 'query-string'
const OperationPage = (): JSX.Element => {
const { search } = useLocation()
const queryStringsObject = queryString.parse(search)
const { operation, location } = useParams<{ operation: string; location: string }>()
console.log(
'>>>>>queryStringsObject',
queryStringsObject,
'\n search:',
search,
'\n operation:',
operation,
'\n location:',
location
)
return <div>component</div>
}
const mapStateToProps = (state) => {
return {
test: state.test,
}
}
export default connect(mapStateToProps, {})(OperationPage)
terminal where the tests are running
>>>>>queryStringsObject [Object: null prototype] {
business: 'freelance',
businessId: '1',
pageId: '1',
pageName: 'Trello'
}
search: ?business=freelance&businessId=1&pageId=1&pageName=Trello
operation: integrations
location: trello
PASS src/__tests__/operations.spec.tsx
Operation Page - Route
ā should load (48 ms)
Test Suites: 1 passed, 1 total
Tests: 0 skipped, 1 passed, 1 total
Snapshots: 0 total
Time: 2.365 s
Ran all test suites related to changed files.
Solution 8 - React Router
If using the enzyme
library, I found a much less verbose way to solve the problem (using this section from the react-router-dom
docs):
import React from 'react'
import { shallow } from 'enzyme'
import { MemoryRouter } from 'react-router-dom'
import Navbar from './Navbar'
it('renders Navbar component', () => {
expect(
shallow(
<MemoryRouter>
<Navbar />
</MemoryRouter>
)
).toMatchSnapshot()
})