Test hooks on “keyboardDidShow” in React Native Functional Component

FanchenBao
5 min readJan 5, 2021

Update 01/05/2021

I have ditched the enzyme solution and fully embraced @testing-library/react-native , which makes testing hooks and triggering events much easier. The same testing as shown in the original article can be written in just a few lines and without the need to mock.

Updated testing code using @testing-library/react-native

Recently, I need to write tests for a functional component that uses useState and useEffect to toggle an internal state based on whether the virtual keyboard shows up. A simplified version of the functional component is displayed below.

Source code for the sample component

The basic functionality is that a piece of text is shown by default. However, if the virtual keyboard is pulled up, the text disappears.

Tricky Part I

Although the component itself is not complex, testing it proves to be quite tricky using jest and enzyme. The tricky part is how to trigger the "keyboardDidShow" and "keyboardDidHide" events. If an event is associated with a component, such as the "click" event in a button, one can use the simulate API in enzyme to trigger it directly (e.g. this article or the keyup keydown event trigger discussed here). However, the "keyboardDidShow" and "keyboardDidHide" events are not associated with any component, thus the simulate API won’t work. This SO answer provides an alternative solution, but unfortunately it does not work for me.

Another method I have come across suggests using react-hooks-testing-library. Yet the documentation specifically mentions that this library is not suitable for testing hooks defined inside a component. So it is not the way to go.

Workaround for Tricky Part I

A workaround is finally found in this article. Very cleverly, the author uses a map to mock the event listener registry. Once all the callbacks are registered to their corresponding event triggers, the event-to-callback mapping is available in the map. Thus, instead of triggering an event and have the event listener call the callback, we can directly call the callback as if the event had been triggered. Let’s take a look at the testing code to have a better understanding of this method.

Testing code using enzyme and various mocking

Line 10 sets up the map keyboardCallbackMap. Line 9 puts a spy on Keyboard.addListener so that we can mock its implementation later. Line 32 mocks the implementation, and we can see that the event-to-callback mapping is recorded in keyboardCallbackMap. To better illustrate this point, we can log the content of keyboardCallbackMap after MyComponent is mounted, and this is the output we will get:

{
keyboardDidShow: [Function: _keyboardDidShow],
keyboardDidHide: [Function: _keyboardDidHide]
}

We have access to the two callbacks defined inside MyComponent. Line 37 calls _keyboardDidShow callback directly from keyboardCallbackMap, as if the "keyboardDidShow" event had been triggered. Similarly, line 39 calls _keyboardDidHide as if the "keyboardDidHide" event had been triggered.

Tricky Part II

Now that we have resolved the issue of “triggering” the "keyboardDidShow" and "keyboardDidHide" events, how do we test their effects? Note that the effects of these two events’ callbacks are associated with the internal state. Therefore, the effect of the events can be tested through the state change of the component. However, testing state change in a functional component is tricky, because the state itself is not accessible (functional component is stateless). We will have to test some side-effect associated with the state change. Unfortunately, there is no easy-to-test side-effect in MyComponent. The only side-effect is that the entire component appears or disappears upon keyboard hiding or showing, which is rather difficult to test in enzyme (see Other Tests for more details).

Workaround for Tricky Part II

The workaround is to mock useState. Since the _keyboardDidShow and _keyboardDidHide callbacks both call setShowText with different arguments, we can mock useState and setShowText; this way the effect of the keyboard showing or hiding can be examined on the calling status of setShowText. Referring back to the testing code, line 7 and line 8 set up the mock mockUseState and mockSetShowText. Line 35 mocks the implementation for mockUseState. After the component is mounted and the "keyboardDidShow" and "keyboardDidHide" events “triggered”, the keyboard event callbacks will call mockSetShowState. Line 38 and Line 40 test the calling status of mockSetShowText in response to the event triggering.

Other Tests

The discussion above covers the third test (Line 31 to 40). What about the other two tests? What do they do?

The first test from Line 18 to 23 tests the default behavior of the component. Without any manipulation, the component should display the text. This is a straightforward test with no trick involved.

The second test from Line 25 to 29 tests whether the text disappears if showText is set to false. On Line 26, we use a trick to set the default value of showText to false, thus ensuring that no text will be displayed. This apparently is a hack, because the ideal testing should be triggering the "keyboardDidShow" or "keyboardDidHide" events, and then testing whether the text disappears or shows up, respectively. Although we already have a way to “trigger” these events, the mounted MyComponent cannot be updated to reflect the change in showText. I guess this has something to do with MyComponent being stateless. Since no state is stored in the mounted component, change of the internal state also cannot be manifested. Hence, the only way I can test the component’s behavior in response to change in showText is to initialize it with different values, which is exactly what line 26 does.

Finally, in the third test, there is an additional line 41, which unmounts the component. Its purpose is to trigger the event listener remover. If any test is needed on the remover, one can write it after line 41. In my case, I don’t need to test the remover, so nothing follows that line.

Conclusion

In this article, we see how the "keyboardDidShow" and "keyboardDidHide" events can be “triggered” using their callbacks that are recorded in an external map. We also see how useState can be mocked to test the effect of event callbacks when they are connected to the internal state change. Finally, we discuss a trick to examine the effect of internal state change by initialization the state with different value, instead of changing the state after the component is mounted.

--

--