Test React Native -Clean & Fast guide ⭐️

MJ Studio
MJ Studio
Published in
10 min readJun 10, 2020

--

Who reads this?

  • who agrees that the test code in the project is almost good but not always
  • who knows Jest basic syntax
  • who knows basic test code concept like mocking, unit test
  • who really want to write graceful React Native test code

Why I wrote this posting?

First, I don’t want to re-teach why the test code is needed in the project deeply or what is TDD or BDD.

What is the most painful dilemma for developers? It is a fight between the due date and writing test code. At first glance, the two things seem to be incompatible with each other. At this time, a lot of developers think like “I know the shiny of the test code in the project absolutely, but I don’t have the time to writing test code. Because my boss wants the application of our company to have new features at least within a week.

Ok, the example above is a sad and common story. We are making our future darker ourselves because of external pressure. But, you need to know that the writing test code is not for your boss or company, it is for the developer(you!). Debugging the errors from the application already shipped to the store is almost a nightmare. It is not only a fight with only your code but also customers.

https://deepsource.io/blog/exponential-cost-of-fixing-bugs/

I know what is the problem. In the real world situation, there are few bosses who know what is great code for future business expansion or stable management. In this situation, If you want to write test code in your project, then you have two options.

  1. You have the skill to convince your boss about writing test code.
  2. You are a genius and can develop new features within 2days.
  3. You can write test codes very fast.

Option 1 is not almost about you. This is affected by your team’s attitude about the development process. Are you in a team that thinks test code as a matter? Then you are lucky. If you are in option 2, you are maybe not reading this or close this now.

How about option 3? It is entirely about you. This option can be chosen in any team and situation. But it is not an easy one. You have to familiar with the test library and attribute of your language. In React Native, they are jest and Javascript.

I think, be a skillful developer is strongly related to option 3. With this, you can make your project more testable and more reliable to yourself. I will introduce some tips and recipes about how to write clean and fast test code with React Native project.

Tools

Before learning more practical test code, I will use some tools for writing test codes.

Jest

Jest is the most popular Javascript test runner library. It provides several CLI commands and configurations about the test environment and intuitive API about mocking or asserting.

testing-library/native-testing-library

This library provides API that renders your component in a deep manner. callstack/react-native-testing-library is also a good option. The APIs both are not very different and both render components deeply.

jefflau/jest-fetch-mock

jest-fetch-mock is a mock utility for fetch API. It provides a simple API that mocking request or response of our networking.

Tips

Check is test environment in code.

You can create condition blocks that should be executed only test environment.

export function isTestEnvironment(): boolean {
return process.env.JEST_WORKER_ID !== undefined;
}

Setup each test environment.

In Jest, the test environment means that each module(a file). In the jest.config.js Jest configuration file, you can add a common environment setup process to each test environment(a file).

jest.config.js

module.exports = {
...
,
setupFiles: [
'./node_modules/react-native-gesture-handler/jestSetup.js',
...jestPreset.setupFiles,
'<rootDir>/test/jestSetupTouchable.js',
'<rootDir>/test/jestSetupCreateDrawerNavigator.js',
'<rootDir>/test/jestSetup.ts',
'<rootDir>/test/apiMock.ts',
],
...
};

test/jestSetup.ts

jest.useFakeTimers();
Date.now = jest.fn(() => 1503187200000);

jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
jest.spyOn(SafeAreaContext, 'useSafeArea').mockReturnValue({ bottom: 0, left: 0, right: 0, top: 0 });

/**
* Native Modules
*/
jest.mock('../src/utils/ScreenBlock');
jest.mock('../src/utils/ScreenLock');

Mocking just a function in the module with spyOn

jest.spyOn is a great mocking API in Jest. I think it is more simple and powerful more than jest.mock . It is not hoisted during the test and can restore. Sure, jest.mock is also have doMock or unMock . But the catching timing when mocking and unmocking makes me confuse in the complex test code file.

Imagine you have a sample module like the following.

samplemodule.ts

export function sum(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}

If you want to mock only sum but not subtract, there are several options to select.

sampletest.test.ts

import * as Calculator from './samplemodule';

jest.mock('./samplemodule', () => {
const actual = jest.requireActual('./samplemodule');
return {
...actual,
sum: () => 2,
};
});

describe('group', () => {
it('sum is mocking', () => {
expect(Calculator.sum(1, 5)).toBe(2);
});

it('subtract is not mocking', () => {
expect(Calculator.subtract(1, 5)).toBe(-4);
});
});

But imagine that in the other test case, you want to use actual sum function implementation instead of mocked.

import * as Calculator from './samplemodule';describe('group', () => {
it('sum is mocking', () => {
expect(Calculator.sum(1, 5)).toBe(2);
});

it('subtract is not mocking', () => {
expect(Calculator.subtract(1, 5)).toBe(-4);
});

it('I want to use actual sum function', () => {
jest.unmock('./samplemodule'); // Wrong️️ ❗️
jest.dontMock('./samplemodule'); // Wrong️️ ❗️
jest.restoreAllMocks(); // Wrong ❗️
jest.clearAllMocks(); // Wrong ❗️

/**
* Error: expect(received).toBe(expected) // Object.is equality

* Expected: 6
* Received: 2
*/
expect(Calculator.sum(1, 5)).toBe(6);
});
});

All methods used in the above are not working well. Why? the jest.mock is hoisted and Calculator the module is already imported. Dynamic mocking or unmocking is not an easy one. But if you use jest.spyOn it is so simple.

import * as Calculator from './samplemodule';

describe('group', () => {
it('sum is mocking', () => {
jest.spyOn(Calculator, 'sum').mockReturnValueOnce(2);
expect(Calculator.sum(1, 5)).toBe(2);
});

it('subtract is not mocking', () => {
expect(Calculator.subtract(1, 5)).toBe(-4);
});

it('I want to use actual sum function', () => {
expect(Calculator.sum(1, 5)).toBe(6);
});
});

We can mock sum function in the first test case only. If you want to restore mocked implementation manually, then you can use returned object of jest.spyOn and invoke restoreMock() .

Prepare test component

When testing components, in React Native Testing Library, you should call render function for the component as SUT.

const {baseElement, getByText } = render(<Screen {...props} />);

The only required parameter of render is a React element we should test(Also you can pass options like wrapper components to the second parameter). If your component used many useContext hooks or React Navigation hooks like useNavigation , then rendering only your component won’t be rendered correctly. Because your component requires <Provider> s wrap your component at before like next.

const {baseElement, getByText } = render(
<MyProvider>
<YourProvider>
<
Screen {...props} />
</YourProvider>
<MyProvider>
);

If we have 10 Provider s, then preparing to render the test component won’t be fun. You can make a test utility function at this moment.

preloadState is for redux store data mocking during before render component and withNavigator is for adding the wrapper NavigationContainer for using useNavigation or useFocusEffect hooks in React Navigation library. Then you can use the above functions.

let props: any;
let component: ReactElement;
let testingLib: RenderResult;


beforeEach(() => {
props = createTestProps();
component = createTestElement(<Screen {...props} />);
testingLib = render(component);
});

If we rendering our test component and save it to global variable, then you can skip rendering boilerplate in each test case. But if you want to mock some features, then you have to mock first and write rendering code.

Skip async operation warning

If you write like the following. Then the annoying warnings will be gone.

afterEach(() => Promise.resolve());

Place Test related files to the same path.

I recommend to place .test.ts or .snap files on same path of the tested file. It is useful when you mock modules because of the equality of mocked path in the tested file. To place .test.ts file is easy. But .snap file is created automatically. You can adjust generated place of .snap file with snapshotResolver property in jest.config.js

jest.config.js

module.exports = {
...

snapshotResolver: './snapshotResolver.js',
};

snapshotResolver.js

module.exports = {
// resolves from test to snapshot path
resolveSnapshotPath: (testPath, snapshotExtension) => testPath + snapshotExtension,
// resolves from snapshot to test path
resolveTestPath: (snapshotFilePath, snapshotExtension) => snapshotFilePath.slice(0, -snapshotExtension.length),
// Example test path, used for preflight consistency check of the implementation above
testPathForConsistencyCheck: 'some/example.test.js',
};

I am using WebStorm IDE and it supports fold related files. It looks great.

Recipes

Network API Call 🔥

The unit tests have to be always passed if it was passed before without any code changes. It seems to be a meaningless testing network API because it is should be mocking in the entire test. But the testing whether the network request has a valid parameter or URL is important. I write test code about that.

First, we need to mock fetch API. If you use axios or any other network client API, then there are mocking libraries for them. I will use jest-fetch-mock .

jestSetup.test

import FetchMock, { GlobalWithFetchMock, enableFetchMocks } from 'jest-fetch-mock';

declare const global;

const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
enableFetchMocks();
customGlobal.fetch = FetchMock;
customGlobal.fetchMock = FetchMock;

You should place jestSetup.test file to setupFiles property in jest.config.js .

Then, I created a simple utility function that mocks fetching and can be used to validate the request.

Then you can write test code like this.

mockFetch utility function does many things. It can mock the response body or check that the API code is sent correctly.

Component 🔥🔥

To test components, you have to use React Native Testing Library. It has a similar API with react-test-renderer . Testing React Native components are complex. The range of components includes our screen, shared presentational component, complex component maybe would call API call or components use many hooks which are closely related to platform API.

Let me show you the next screen component.

This screen component is used to select your profile photo and save it with a network API call. Let’s write a test code about this one.

1. Snapshot testing

The snapshot testing is a feature of Jest itself. It will compare current object and previous cache. This is useful to testing component looks like.

it('compare snapshot', () => {
const { baseElement } = testingLib;
expect(baseElement).toBeTruthy();
expect(baseElement).toMatchSnapshot();
});

toMathchSnapshot will compare current baseElement object and content of .snap snapshot file. If we changed the text color or background color, then the test will be failed. If there is no snapshot file yet, the new snapshot file will be created and the test will be passed.

2. Press back button should call goBack of React Navigation.

3. Test selected photo is passed to API request.

With only three simple tests, we can cover our PhotoSelect screen component almost 95%

Hook 🔥🔥🔥

In the testing hooks, you have 2 options.

  1. react-hooks-testing-library is a simple and great library for testing hooks. But in this readme, they defined when to use this library.

2. Options 2 is hooks are placed in When not to use this library in the above photo. We can create a simple test component and test hooks using interaction with the component.

I won’t describe the option 1. You can read the more clever document in the library readme. Let me show an example of option 2.

I created a simple in app event listener hooks for emitting global event to any other screen.

useEventListener.ts

const useEventListener = <T>(type: EventType, listener: Listener<T>, unsubscribe?: () => void) => {
const listenerRef = useRef<Listener<any> | null>(null);
const unsubscribeRef = useRef<() => void>();

listenerRef.current = listener;
unsubscribeRef.current = unsubscribe;

const listenerCallback = useCallback((payload: any) => {
listenerRef.current!(payload);
}, []);

useEffect(() => {
AppEvent.addEventListener(type, listenerCallback);

return () => {
unsubscribeRef.current?.();
AppEvent.removeEventListener(type, listenerCallback);
};
}, [type, listenerCallback]);
};

You can ignore EventType and AppEvent . They are just utility. How to test this hook? I created a simple test for that. At first, you can create a component for testing.

useEventListener.test.tsx

const ListenerComponent = ({ eventType, callback, unsubscribe }) => {
useEventListener(eventType, callback, unsubscribe);

return null;
};

Then you can test callback is invoked! It is easy.

Done 😄

Provider 🔥🔥🔥

I don’t like the testing provider itself. Because I think the features of providers should be tested automatically when I test screens. But if you want to test the provider itself, then you can write the test component using a provider like a method in testing hooks.

Where to go?

I am glad to share my React Native test code experiences with you. Did you have a fun time? What’s your attitude about test now? There are numerous testing libraries and testing methods and things that should be tested.

Don’t worry about making the test range of 100%. It is just a number game. The most important thing ensures your shiny future of the development process is determining what should be tested and how test codes make you Confident to go to the next step. 💎

Finally 300 tests!

--

--