Scroll to top

Getting Started with Testing Hooks on React Native


DigitalOnUs - January 14, 2021 - 0 comments

Since hooks were released on v16.8.0 our codebase suffered a big change. Classes started to become extinct due to the easy way to declare a component without a lot of code.
One of the coolest things in hooks is that it is really easy to create reusable components. For example, on my current project, here’s a bunch of hooks that I can reuse anywhere:

useLocalization
● useInterval
● useFullBrightness
● useAppState
And these are just a few. I hope with just the names you can understand the objective of each one of them. After creating our reusable components we decided to start implementing testing in the project, the first step being unit testing. That seemed so easy since our hooks were functions that returns or implements a component – easy peasy. Well, we did not know that testing hooks as a unit – I mean without mounting a component – was not possible without using a third party library in order to write tests for hooks that are not tied to a specific component. So, React hooks testing library + Jest were the chosen ones to make it happen.

Anyways, let’s get started.
React hooks testing library

“Allows you to create a simple test harness for React hooks that handles running them within the body of a function component, as well as providing various useful utility functions for updating the inputs and retrieving the outputs of your amazing custom hook. This library aims to provide a testing experience as close as possible to natively using your hook from within a real component.”
(https://github.com/testing-library/react-hooks-testing-library)

That’s the definition of the library. First of all we need to install it using npm. You need at least React v16.9.0, and be aware about your current project version:

npm install --save-dev @testing-library/react-hooks

That’s all about it. Just remember that Jest should be configured in our React Native project that comes by default. This is how your package.json should look like:

Inside your scripts

"test": "jest",

Double check the jest config

"jest": {
"preset": "react-native"
}

Also, don’t forget to have your dependencies for Jest

"babel-jest": "^25.1.0",
"jest": "^25.1.0",

Note: This is a very simple config for your tests. In order to start mocking or doing something more advanced you need to look into the Jest configuration docs.

Example

In order to explain how to use this library in your project we are going to use an example to start testing the current app state. We should use AppState to check if the app is either active or running on the background, so let’s take a look at this generic hook:

useAppState.js

import { useEffect, useState } from "react";
import { AppState } from "react-native";
export default function useAppState({ onForeground, onBackground, onChange, }) {
const [appState, setAppState] = useState(AppState.currentState);
useEffect(() => {
const handleAppStateChange = (nextAppState) => {
if (nextAppState === "active") onForeground?.();
else if (appState === "active" && nextAppState.match(/inactive|background/))
onBackground?.();
setAppState(nextAppState);
onChange?.(nextAppState);
};
AppState.addEventListener("change", handleAppStateChange);
return () => AppState.removeEventListener("change", handleAppStateChange);
}, [onBackground, onChange, onForeground, appState]);
// Used for testing
useEffect(() => {
onChange?.();
}, [appState, onChange]);
return { appState, setAppState };
}

As you can see, we have this component in order to check if the app changes its state, I mean if it switches from active to background and vice versa, the component calls to the function that is passed as a prop in order to tell the component where it is being used; the app state just changed. Also, we have two flags that are going to help us to validate our tests: useEffect with the onChange call, and the setter setAppState, which we’ll discuss later.

Now, let’s check how to test this component as a unit without mounting it inside of a component. We’ll have to import two functions from our testing library: act and renderHook.

renderHook: Let us mount a hook with some additional parameters

function renderHook(
callback: function(props?: any): any,
options?: RenderHookOptions
): RenderHookResult

act: Used to prepare our component in order to perform updates in it

act(() => {
performSomeComponentUpdate()
});

Let’s see in action. We need to create our appState.test.js file and then import the utility among our appState.js component:

import { act, renderHook } from "@testing-library/react-hooks";
import useAppState from "./useAppState"; // This could change according to your folder structure

It’s important to define our 3 possible states that our app could take:

const APP_STATE_VALUES = {
ACTIVE: "active",
BACKGROUND: "background",
INACTIVE: "inactive",
};

Now, it’s time to create our test body. This time I am not going to use any lifecycle test in our file, so let’s write the skeleton including the first test using renderHook in order to mount the hook.

describe("useAppState test", () => {
it("Should render ok", () => {
const { result } = renderHook(() => useAppState());
expect(result.current).toBeDefined();
});
});

That is our very basic test. But is our initial point in order to know the result object that renderHook returns? Let’s see:

{
"result":{
"current": {} // This object contains the values that our hook returns, either a function or a value
}
}

Given that, we’ll write a second test case. It will check if the state is changing according to the AppState value, but this time I am going to manipulate the value manually since we’re running a unit test and the AppState should be undefined. Also, we’re passing in onChange function as a prop in order to validate the correct setState. So, let’s take a look on the next piece of code:

it("Should change the app state and call onChange on every update", () => {
const onChange = jest.fn();

AppState.currentState = APP_STATES.ACTIVE;
const { result } = renderHook(() =>
useAppState({
onChange,
}),
);

expect(result.current.appState).toBeTruthy();
expect(result.current.appState).toEqual(APP_STATES.ACTIVE);

act(() => {
result.current.setAppState(APP_STATES.BACKGROUND);
});

expect(onChange).toHaveBeenCalled();
expect(result.current.appState).toBeTruthy();
expect(result.current.appState).toEqual(APP_STATES.BACKGROUND);

act(() => {
result.current.setAppState(APP_STATES.INACTIVE);
});

expect(result.current.appState).toBeTruthy();
expect(result.current.appState).toEqual(APP_STATES.INACTIVE);
expect(onChange).toHaveBeenCalledTimes(3); // 3, avoiding the first mount and the first call
});

I know that’s a lot of new syntaxes to digest. But basically we’re creating one more test case, manipulating the current AppState value and setting it to active. 

AppState.currentState = APP_STATE_VALUES.ACTIVE;

This is really necessary due to a lack of a real scenario. As I said in the code above, we’re just running in a test environment. After that I’m repeating the same line as the first test case, with just mounting our hook using the testing library. The next two assertions are to validate that our state is really active:

expect(result.current.appState).toBeTruthy();
expect(result.current.appState).toEqual(APP_STATE_VALUES.ACTIVE);

Here comes the weird part (if you have not seen the act method on any test library).  Act is responsible to do the transition work. I mean, if the method/action you are calling is going to modify the internal state of a component it should happen here. That’s why I’m calling the setAppState method that belongs to our reusable hook inside the act function. We just need to pass a callback function as unique parameter and the magic happens:

act(() => {
result.current.setAppState(APP_STATE_VALUES.BACKGROUND);
});

Last but not least, we should validate that onChange is being called after the app state is changed. (this is only for testing purposes.  It is not really necessary to do tests – here, but a double check wouldn’t hurt):

expect(onChange).toHaveBeenCalled();

Next, we’re triggering the value for AppState to background. (Don’t worry about the setAppState method. it is not used throughout the project; it’s just for testing purposes). In a real scenario the appState should change when the user comes from background to active and vice versa. But, we need to mock in order to check if our file really works. So, let’s move forward.

On the lines below we are going to repeat the same., After validating that our appState value is changing according to what we tell it, we change it again, this time to inactive and then we validate that the appState is not null (toBeTruthy()) and it’s equal to inactive. Also, don’t forget to double validate that onChange is being called again.

expect(result.current.appState).toBeTruthy();
expect(result.current.appState).toEqual(APP_STATES.BACKGROUND);
act(() => {
result.current.setAppState(APP_STATE_VALUES.INACTIVE);
});
expect(result.current.appState).toBeTruthy();
expect(result.current.appState).toEqual(APP_STATE_VALUES.INACTIVE);
expect(onChange).toHaveBeenCalledTimes(3);

And that wraps it up. Just save your code and run the test with:

npm run test

Or your running command for testing previously configured in your package.json file.

The end result should look like this.The end result should look like this.

Related posts