This article is based on a webinar that was hosted by HeadSpin in June 2019. Be sure to check out the recording as well!
React Native is a response to the age-old problem of how to get an agreeable cross-platform mobile app developer experience while not sacrificing user experience or perceived performance. Developers love building apps with a single set of tools and methodologies, and this of course means web development tools and methodologies, since the web is the canonical universal platform. Unfortunately, building native apps on different mobile platforms (Android and iOS) often involves using a host of different tools and methodologies, based on whatever Google or Apple dictate to their developer communities.
Choosing to build a hybrid app was always a way out of the double-platform problem, but hybrid apps came with lots of perceived UX issues. This is where React Native shines: rather than writing web-like code which actually runs in a webview, you use React Native to write web-like code which is interpreted on the fly inside of a native mobile app, which creates native UI components and response, and which responds to native UI events. The end-user experience is (at least in principle) exactly the same as any native mobile app built with the first-party platform SDKs, but the developer experience is that of a web developer, writing in React-flavoured JavaScript, and using JSX along with a CSS equivalent for structure and styling.
How does React Native accomplish this best of both worlds feat? Between the app code and the user experience it interposes several unique modules, as depicted in the following diagram:
Basically, when a React Native app is launched, regular old native app code is run. You don't write this code---it's maintained as part of the React Native project. In addition to these native (per-platform) modules, React Native apps come bundled with a JavaScript engine called JavaScript Core. The code you write is executed by this JS engine, and any time this results in a UI update, a message is passed to the native modules via a special message bus called the React Native Bridge. Once the native modules again have control, they are free to update the UI, create native UI components, or listen for user input, just as any native app would.
This multi-part architecture certainly adds internal complexity to the app, but its complexity which is hidden away by the architecture. A React Native app developer can usually ignore it, and just live in the world of React.js, all the while building apps which produce native user experiences. Well, mostly. At the end of the day, these apps still need to be compiled as iOS .ipa
files or Android .apk
files. They still need to have the appropriate app manifests and resources in the appropriate places. So there is some amount of platform-specific wrangling that still needs to be done, even with React Native. How much exactly depends on your specific app, and how differently-designed you want your apps to be. But in general, a more accurate picture of the architecture, including your responsibilities, is as pictured here:
Based on my experience maintaining a React Native app (The App, which is used for almost all of Appium Pro's examples), these are the pros and cons of using React Native for mobile app development:
Pros | Cons |
---|---|
Mostly single codebase | A lot of "mosts"—need some iOS/Android experience |
Mostly no need to learn iOS or Android app dev | Reliant on RN team for SDK compatibility updates |
Good debugging toolkit for rapid development | Difficult to write truly platform-specific UX |
Mostly native app speed / UX | Reliant on project owned by FB (though now under MIT license) |
Extensible—write your own native modules |
As with any kind of app, there are different levels of testing to consider, from unit testing up to end-to-end or UI testing. The most popular option for unit testing React Native apps is Jest, produced by Facebook (same as React Native). This kind of testing doesn't involve UI components at all. Jest can be combined with another library (like react-native-testing-library), to get a little more fancy and test at the component level (though again, this is not testing involving native UI components, only virtual components).
When it comes to UI testing, Appium is a great option for testing React Native apps. React Native apps just are native apps, therefore they're testable out of the box with Appium. React Native components even respond to a testID
attribute that allows developers to attach test-friendly labels to components. However, there are a few things to consider when developing React Native apps to make sure that they are fully testable with Appium. In the Appium Desktop screenshot below, for example, you will notice a pretty substantial problem with this React Native app:
The problem is that, even though I can clearly see many elements with lots of unique text labels and so on, all I can find in the XML hierarchy is a single android.widget.FrameLayout
! The reason for this is that Appium's element finding algorithms operate on the UI Accessibility layer, and the designer of this React Native app has designated certain elements as important for accessibility, thus rendering many other elements un-important for accessibility. Oops!
The solution here is to make sure not just to use the testID
attribute on important components, but also to set the accessibilityLabel
attribute, to ensure that the element is always findable via Appium's 'accessibility id' locator strategy. (Actually, this is only a problem on Android, because on Android, React Native puts the testID
into something called a 'view tag', which is not accessible to Appium at all.
I like to make a convenience function called testProps
to make this process easy:
export function testProps (id) {
return {testID: id, accessibilityLabel: id};
}
Now I can just use this function everywhere instead of testID
:
<Component {...testProps('foo')} />
Another tactic for dealing with this issue, if we don't want to add labels and IDs to lots of components, is to make sure that parent components have their accessible
attribute set to false
; this allows the accessibility visibility of children to bubble up through a parent, rather than treating the parent component as a monolithic component from the perspective of the accessibility system.
That's really it! I haven't run into any other significant issues testing React Native apps with Appium. But check out the webinar for a more in-depth discussion, and a comparison of Appium with the Detox UI testing framework, which has some special integrations with React Native specifically.