React Native & Jest: Mock Platform OS and color scheme
When you have a React Native application and want to feature test your components. Sooner or later, you will check how the component renders from an iOS, iPad or Android device.
The tricky part is not adding support to the device. That comes by default from the framework. The complicated part is to tell the testing framework that you want to render a component in a particular device.
Here I’m going to list the solutions I found trying to add those.
🔨 What libraries am I using?
For this purpose, I’m currently using the following framework versions:
- React: 18
- React Native: 0.70.2
- Jest: 29.1
- Typescript: 4.8
-
@testing-library/react-native
: 11.2.0 (for testing JSX components)
💻 Mocking device OS
Detecting OS as a helper function
As you might know, we have the Platform.OS
option to detect over which platform we are running our code. To make testing and/or reusing the code in our project more straightforward, I prefer to have a “Helper” file. For the next example, I’m using ESM syntax, but you can still use CommonJS module one.
Let’s add a static method so we won’t need to instantiate the class. Rather than new Helpers().isIpad()
we can just use Helpers.isIpad()
.
Now, how can we test this simple function against different devices and operating systems? Let’s see the next test that I wrote after many hours of struggling to find overrides.
I’m also adding some edge cases to try to reach 100% of the code’s coverage case.
Remember that react native would still detect an iPad device as an
ios
device.
Writing the tests
Let’s test that our function can run over Android.
As you can see, we can use Platform.OS
to override our platform per test.
An iPad device will be treated as they use ios
operating system. So, how do we force React Native always to retrieve true
in its Platform.isIpad()
function?
Here is where spies come to help us. Since isIpad
is a getter function, this is an easier way to override it. Then I’d prefer to use mockReturnValueOnce
here to specify when the functions execute once, it will retrieve that first time the value that I need.
For Typescript, we will use PlatformIOSStatic
. This is a way to tell TS that Platform
types (and functions) will be referring to an iOS / iPad device.
Once we learned that, we could add some tests to cover any uncommon or edge case.
Test pollution
Since some tests can run in parallel, we can have some test pollution. It means that some tests values could override other ones from the same group. Do you see how I need to do a Platform.OS = 'ios'
for every test? If you want a default platform, you can wrap your test in a describe
function and add an afterEach
. With this, we can override the platform and set an initial one after every test.
🎨 Mocking useColorScheme hook
Ok, now we can override the platform. But what about dark mode? Whether you are using Android or iOS, dark mode will be useful, especially at night or in dark environments. From React Native, we can conditional code some styles based on that. And we have native access to the useColorScheme
hook. But how can we mock it?
First, let’s see an example using the hook to implement the dark mode inside a component.
My ColoredStatusBar component
This function changes the color of the status bar, only validating if the OS is ios
and with light mode. That file GlobalStyles
are React Native style declarations as an object. The useIsFocused
is a React Navigation hook to detect if the current app is focused.
Current hook implementation
If we go to the source code of that useColorScheme
we will find the following code. This is React Native code at the version specified before.
They are using ESM exports to export a function by default. Mocking it would be different.
Let’s see how we can snapshot test the component.
Creating a snapshot test if device is iPad but in Dark Mode
Some notes here:
- We use
doMock
to avoid test pollution. See some docs here . And we returndark
at first call, because if we don’t mock it, it will returnlight
by default. - We are testing that at least we are calling
useIsFocused
hook once (1st render). - We use
toJSON
to snapshot test the component.