[React Testing] Test react-router-dom Router Provider with createMemoryHistory

Mocking the <Redirect /> component in react-router works, but it’s imperfect because we don’t know for sure that the user will be redirected properly. Alternatively, we can render our component within a Router with a custom implementation of a history via createMemoryHistory. Then we can make assertions on that history object.

Component:

import React from 'react'
import { Switch, Route, Link } from 'react-router-dom'

const About = () => (
  <div>
    <h1>About</h1>
    <p>You are on the about page</p>
  </div>
)
const Home = () => (
  <div>
    <h1>Home</h1>
    <p>You are home</p>
  </div>
)
const NoMatch = () => (
  <div>
    <h1>404</h1>
    <p>No match</p>
  </div>
)

function Main() {
  return (
    <div>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route component={NoMatch} />
      </Switch>
    </div>
  )
}

export { Main }

Test:

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { createMemoryHistory } from 'history'
import { Router } from 'react-router-dom'
import '@testing-library/jest-dom/extend-expect'
import { Main } from '../extra/main'

test('main renders about and home page and I can navigate to those page', () => {
  const history = createMemoryHistory({ initialEntries: ['/'] })
  const { getByRole, getByText } = render(
    <Router history={history}>
      <Main />
    </Router>,
  )
  expect(getByRole('heading')).toHaveTextContent(/home/i)
  fireEvent.click(getByText(/about/i))
  expect(getByRole('heading')).toHaveTextContent(/about/i)
})

test('landing on a bad page shows no match component', () => {
  const history = createMemoryHistory({ initialEntries: ['/no-match-router'] })
  const { getByRole } = render(
    <Router history={history}>
      <Main />
    </Router>,
  )
  expect(getByRole('heading')).toHaveTextContent(/404/i)
})

An improved version:

import React from 'react'
import { Router } from 'react-router-dom'
import { createMemoryHistory } from 'history'
import { render, fireEvent } from '@testing-library/react'
import { Main } from '../extra/main'
import '@testing-library/jest-dom/extend-expect'

// normally you'd put this logic in your test utility file so it can be used
// for all of your tests.
function renderRoute(
  ui,
  {
    route = '/',
    history = createMemoryHistory({ initialEntries: [route] }),
    ...renderOptions
  } = {},
) {
  function Wrapper({ children }) {
    return <Router history={history}>{children}</Router>
  }
  return {
    ...render(ui, {
      wrapper: Wrapper,
      ...renderOptions,
    }),
    // adding `history` to the returned utilities to allow us
    // to reference it in our tests (just try to avoid using
    // this to test implementation details).
    history,
  }
}

test('main renders about and home and I can navigate to those pages', () => {
  const { getByRole, getByText } = renderRoute(<Main />)
  expect(getByRole('heading')).toHaveTextContent(/home/i)
  fireEvent.click(getByText(/about/i))
  expect(getByRole('heading')).toHaveTextContent(/about/i)
  // you can use the `within` function to get queries for elements within the
  // about screen
})

test('landing on a bad page shows no match component', () => {
  const { getByRole } = renderRoute(<Main />, {
    route: '/something-that-does-not-match',
  })
  expect(getByRole('heading')).toHaveTextContent(/404/i)
})