E2E Tests in React Native in a GraphQL App
Motivation
The goal of this article is to learn how to write end-to-end tests in a React Native application using the test framework Cavy. For this, we are going to give a brief introduction to Cavy and Cavy-CLI, how to adapt our code with the hooks Cavy provides, and how to mock our GraphQL backend using a mocked provider from Apollo.
First of all, what’s an end-to-end test and why are they so important for our app? An end-to-end test is the way we have to test the whole application from start to end, this simulates a real scenario and how a user would use the app. Doing this, we can prevent potential bugs and ensure that our application works correctly as a whole, including backend, UI and third-party libraries.
Cavy and Cavy-CLI
“Cavy is an open-source end-to-end test framework for React Native, developed for use with both iOS and Android applications.” - Pixie Labs
The way we use Cavy is with React’s ref prop. You must pass the generateTestHook
function that Cavy provides with an identifier of the component, which is going to be used to reference it in the specs. Ok, but how does Cavy run the tests? By swapping our main index.js
entry point, with an index.test.js
which will be used by cavy to boot the app and run the tests. (Setting up our cavy tester).
Then there’s Cavy-CLI, a command-line interface that, among other things, lets us setup Cavy creating an e2e
folder and the already mentioned index.test.js
file, and run the tests of each platform (iOS and Android) using several props. For example, to create the e2e
folder and index.test.js
you run cavy init e2e
, to run the iOS test you simply do: cavy run-ios
, or cavy run-ios --skipbuild
if your app is already running in a simulator. You can see the full list here.
How do we adapt our code to use Cavy?
As we said before, we need to use the generateTestHook
function to register our components. There are two ways to access this function:
- With react hooks and using
useCavy()
to obtain the function. - Using the
hook
function and receivinggenerateTestHook
as a prop in the component.
Since not all components can be assigned with a ref (e.g. third-party libraries), Cavy provides the wrap function to make those ones testable too. To do that the component has to be wrapped like this:
Note: The following React Native components have to be wrapped like this if you want to test them: Text
, TextInput
, Pressable
, TouchableOpacity
and TouchableHighlight
. To avoid doing this every time you want to use one of these components, it’d be better to create a reusable component that returns the component already wrapped: for example TestableTextInput
or TestableTouchableOpacity
.
When writing the specs, Cavy provides a set of helpers, for example:
.findComponent(identifier)
: returns a component using its test hook identifier..fillIn(identifier, string)
: fill in a component with a given string. Really helpful to fill TextInputs ;).press(identifier)
: press the component with the given test hook identifier
You can take a look at the rest of the helpers here.
Mocking our Apollo Client
In case you need to test a flow that calls your API, you have two choices: mock the responses or use the real API. In this section we are going to see how to implement the first choice in a GraphQL environment, you can skip this section if you want to go with the second choice.
To test components that make API calls using Apollo, we are going to need to mock that service using MockedProvider
, a version of ApolloProvider
that doesn’t send real network requests to our API. We specify the response payload we want for a given GraphQL operation and this is used by the mocked provider.
Once we have our main index.test.js
file, we need to configure our provider, in this case: MockedProvider
. Let’s take a look at this file:
Here we have the Tester
component wrapping the whole app, this instantiates a new TestHookStore, which will store references to the testable components registered, and render our app inside Cavy's Tester component so that the tests run on boot.
Then we can see the MockedProvider
component, this is where the magic happens when mocking API calls. This component needs the mocks
prop, which are all the mocked mutations and queries we are going to use in our tests (it is important that all requests you use in the flow be passed in this prop, otherwise the test will fail because the provider won’t find it).
It’s a good practice to have a __mocks__
folder for that, to make our project easier to maintain. You can read more information about mocking requests here.
Important: when mocking a mutation that uses some component’s data, make sure to fill the same data in the components and in the mocked request, this is pretty important, otherwise the mutation will fail.
Last, we put our main navigator inside the MockedProvider, this allows us to also test the navigation flow in our app.
Comparison with Detox
Let’s do a brief comparison between Cavy and Detox, one of the most used test frameworks for React Native.
Installation: Cavy is really easy to install and configure, there is no need to do a different configuration to iOS or Android, we just run the cavy init e2e
command and we are ready to go. It’s a different story with Detox, the installation and setup is different on each platform and it takes a lot of configuration, you need to install another third-party library as a test runner (e.g. jest), and also add special configuration in the package.json
file to specify which devices you want to test (simulators or real devices).
Adapting our code to test: As we said before, Cavy uses the ref
prop to reference the components we want to test, but if you are using this for another purpose, you can still pass it as a second argument to the generateTestHook
function. For Detox, this is almost the same, it uses the testID
prop, but as not all React components accept this prop, you must propagate it through to the correct native component. So, in terms of adapting the code, it is almost the same in both cases.
Capabilities: Cavy is only javascript, so we don’t have the chance to test anything that needs access to native code, for example: opening the camera, access to the gallery, location of the device, tap a specific point, etc. Detox does support this, so it offers more testing functionalities.
Conclusion
To sum up, if you need a fast solution to test basic flows with only javascript code, go ahead with Cavy, it will save you a lot of time for its nature of fast installation and setup in both iOS and Android platforms (this is pretty important to React Native developers, it’s really annoying to spend more time doing different configurations for each one), and the tests are clean and really simple to write.
However, if you are building a big project that needs more specific and native testing, you should go with another testing framework, e.g. Detox, but take into account the time you will need to spend in installation and setup with non cross-platform frameworks.