Testing useNavigate() / navigate() from react-router v6

Testing navigate() is slightly more problematic with the latest v6 (as of writing this post) react-router than just asserting on history.push() as it was the case in the previous versions. Let’s say we have this ButtonHome component:

import { useNavigate } from 'react-router-dom'

const ButtonHome = () => {
  const navigate = useNavigate()

  const onClick = () => navigate('/home')

  return (
    <button onClick={onClick}>
      Home
    </button>
  )
}

I would write a test for this component using the react-testing-library in the following way:

import * as router from 'react-router'
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import ButtonHome from './ButtonHome'

describe('ButtonHome', () => {
  const ui = userEvent.setup()
  const navigate = jest.fn()

  beforeEach(() => {
    jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate)
  })

  it('renders the button and navigates to /home upon click', async () => {
    render(withRouter(<ButtonHome />))
    
    await ui.click(screen.queryByText('Home'))

    expect(navigate).toHaveBeenCalledWith('/home')
  })
})

The relevant bits just for testing the router are as follows:

import * as router from 'react-router'

const navigate = jest.fn()

beforeEach(() => {
  jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate)
})

it('...', () => {
  expect(navigate).toHaveBeenCalledWith('/path')
})

The test also requires the following withRouter() helper, which I have in jest.setup.js:

import { Route, Router, Routes } from 'react-router-dom'
import { createBrowserHistory } from 'history'

const history = createBrowserHistory()

const withRouter = (children, opts = {}) => {
  const { path, route } = opts

  if (path) {
    history.push(path)
  }

  return (
    <Router location={history.location} navigator={history}>
      <Routes>
        <Route
          path={route || path || '/'}
          element={children}
        />
      </Routes>
    </Router>
  )
}

global.withRouter = withRouter

Setting up mocha with sinon and chai

I was unable to quickly find a solution for this, so here’s a little guide on how to set it up together in a proper way.

First, install the libraries:

npm install mocha --save-dev
npm install sinon --save-dev
npm install chai --save-dev

I come from the Ruby world, so I expect to have a spec command, spec_helper.js file and specs living inside spec/ directory (with a nested structure).

Inside package.json file define the spec command:

"scripts" : {
  "spec": "mocha --opts spec/mocha.opts"
}

We will be using BDD style (expect().to()) of chai. Inside spec/mocha.opts add:

--recursive **/*_spec.js
--require spec/spec_helper.js
--ui bdd

Create spec/spec_helper.js, which will require chai and sinon and we will require `spec_helper.js` inside all specs (similarly to how RSpec in Ruby world works).

const sinon = require('sinon')
const expect = require('chai').expect

global.sinon = sinon
global.expect = expect

And now create your spec file (spec/module_spec.js). You should not be required to include any libraries there. Now you can run your specs:

npm run spec

References:

Steve Yegge live

After yesterday’s best read of the month here comes the best show of the day. Which is Steve Yegge, live, talking about branding during 2007 OSCON. Without any slides. 25 minutes.

He also reveals that in his opinion Javascript2 will be the Next Big Language (NBL). I was kind of suspecting this would be it. Coincidently, lately I’ve discovered (what a big word) jQuery. And let me tell you, I’ve never seen any other piece of code, which is so concise and does so many things with so much style. Prototype, dojoToolkit and all other js libraries simply look pale in comparison. I’m hooked on jQuery and use it for every new project, while trying to implement it in my old ones too.