'How to simulate android back button in react-native test

In componentDidMount I'm adding a listener to handle Android back navigation and would like to add tests around it's behaviour. How do I firstly test something in componentDidMount and secondly how would I simulate clicking the back button.

class Component extends React.Component {
  componentDidMount () {
    BackHandler.addEventListener('hardwareBackPress', () => {...})
  }
}


Solution 1:[1]

There are a couple ways you can approach this:

First, you could just assume that componentDidMount and BackHandler.addEventListener are going to work. That would leave you just testing your function. If you go that route, you'd probably want to register a named function, rather than an arrow function, in addEventListener so that you can target it in your test.

If for some reason, you want to test the full lifecycle, then Facebook actually has a mocked out BackHandler that lets you simulate a back press:

https://github.com/facebook/react-native/blob/master/Libraries/Utilities/mocks/BackHandler.js

You'd just need to import that mock into your test, mount the component in a test renderer and then trigger mockBackPress and watch to see if your function is called.

Solution 2:[2]

This is what worked for me based on the mock linked in the other answer:

(I left some notes at the bottom)

import { renderHook, act } from '@testing-library/react-hooks'
import { BackPressEventName, BackHandler } from 'react-native'

import { useAndroidHardwareBack } from '../useAndroidHardwareBack'

jest.mock('react-native', () => {
  const _backPressSubscriptions = new Set()

  const MockBackHandler = {
    exitApp: jest.fn(),

    addEventListener: function (
      eventName: BackPressEventName,
      handler: () => boolean,
    ): { remove: () => void } {
      _backPressSubscriptions.add(handler)
      return {
        remove: () => MockBackHandler.removeEventListener(eventName, handler),
      }
    },

    removeEventListener: function (
      eventName: BackPressEventName,
      handler: () => boolean,
    ): void {
      _backPressSubscriptions.delete(handler)
    },

    mockPressBack: function () {
      let invokeDefault = true
      const subscriptions = [..._backPressSubscriptions].reverse()
      for (let i = 0; i < subscriptions.length; ++i) {
        if ((subscriptions[i] as Function)()) {
          invokeDefault = false
          break
        }
      }

      if (invokeDefault) {
        MockBackHandler.exitApp()
      }
    },
  }

  return {
    BackHandler: MockBackHandler,
  }
})

describe('useAndroidHardwareBack', () => {
  afterEach(() => {
    jest.restoreAllMocks()
  })

  it('calls the back button listener on Android hardware back button press', () => {
    const mock = jest.fn()
    renderHook(() =>
      useAndroidHardwareBack(() => {
        mock()
      }),
    )
    act(() => {
      BackHandler.mockPressBack()
    })

    expect(mock).toHaveBeenCalled()
  })
})

Note:

  • TL;DR: DON'T INCLUDE ALL OF REACT NATIVE IN YOUR MOCK
  • I am testing a hook, but you could also use it to test components.
  • I'm mocking react-native and including only the BackHandler mock. You may need to mock other RN modules if your code depends on them; however, most documentation mentions returning something like this on the mock:
return {
    ...jest.requireActual('react-native'),
    BackHandler: MockBackHandler,
}

Doing this will likely result in a Maximum Callstack Exceeded error.

Solution 3:[3]

For those who are struggling with this and neither answers by edrpls or Garrett McCullough were clear, this is how you do it.

If you don't have already setup a setup.js file for jest, do it.

package.json

"jest": {
    "setupFiles": [
      "<rootDir>/jest/setup.js"
    ],
  }

/jest/setup.js

import mockBackHandler from 'react-native/Libraries/Utilities/__mocks__/BackHandler.js';

jest.mock(
  'react-native/Libraries/Utilities/BackHandler',
  () => mockBackHandler,
);

In the test file where you want to test the BackHandler.

import { BackHandler } from 'react-native';

and you can use the mockPressBack() available from the mock as follows.

BackHandler.mockPressBack();

What's going on?

When you setup a file for jest, every time before a test is run its contents are executed.

In our setup file we import the mock BackHandler object provided by the ReactNative team and use that as the mock for react-native/Libraries/Utilities/BackHandler.

In your component being tested you probably have,

import { BackHandler } from 'react-native';

BUT jest now feeds in the mock object instead of the actual implementation in react native granting you access to mockPressBack() to call it whenever you want to test the BackHandler.

Why the jest setup file? Well, in my personal opinion this BackHandler should be mocked globally once and forgotten about. Otherwise you have to import it and use as a mock per test file.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Garrett McCullough
Solution 2 edrpls
Solution 3 JanithaR