E2E testing with React Native, Jest and Drupal Troubleshooting

Having a background in the fast paced news media industry I learned early of the advantages (necessity) of automated testing which leverage jumping between projects, doing changes and deploying to production with confidence that as long as the tests show green, it’ll be alright. This blog post is about doing this with React Native, GraphQL and Drupal.

Three types of tests

Automated test suites come in different shapes, granular Unit Tests that aim to test each method in a piecemeal fashion. If I pass A to this function, I expect to get B back. This approach was discarded early because it was too cumbersome to maintain, refactoring became a grind having to rewrite bucket loads of tiny test assertions.

Second up was Feature testing, aiming to test a feature as a whole. Outlining a scenario, A user logs in, then presses a button, then X should happen.  It was a improvement, but also this with flaws. Testing a feature in isolation usually means relying on a mocked context, and what ensured the mock data was still what the actual backend would provide? We realized the feature tests might do more harm than good, instilling a false sense of security.

This led up to the third, and so far, preferred option: end-to-end testing, or Black Box testing. The tests would read like feature tests, but with the difference of being run in the context of the actual application, using real endpoints, real database etc. This allowed us to catch all manner of side effects, reaching outside the feature that was being tested: something related to the database, network, server, what-have-you. Once the test coverage of a app was sufficient, a developer who never set foot in that project, could make a change run the tests, deploy and don’t think twice about it! Also because of how the tests are written in a scenario-like fashion, they would double as a technical documentation as well.

Enter Drupal and GraphQL

So, with this in mind, I’d naturally like to leverage that when making a native app with a Drupal backend, served through GraphQL. Besides all the benefit that automated tests bring, in this case it would also aid the team writing the backend. Since a change in Drupal; a field name change, for example, would directly affect the GraphQL endpoint. Since the app tests run towards the real API, any change in the API that would compromise the app would be directly visible since that test would fail.

Sounds good right? Of course, there was some hurdles to overcome. The most problematic one being that GraphQL schema generated by Drupal was _massive_ (about 250k lines). I won’t go into too much detail how Jest and Apollo Client interact, but in brief: Jest test suites run in isolated sandboxes, and to acquire the GraphQL schema in order to execute the queries for the tests the endpoint had to be remotely introspected (analyzed by a tool provided by Apollo, resulting in a schema) by each suite which created major overhead because of the schema size. This process would add roughly 3 seconds, and anyone who dabbled with e2e tests know that speed is a major bottleneck with this approach.

So how to get around that? In the end, what we did is we launched a separate, local Apollo Server dedicated to the tests and applied the schema to it, and the server would in turn proxy any query to the real endpoint. This server would be initialized with a fresh schema prior to the test runs, and serve all the suites before shutting down and disappearing. With only 10 suites at the time, this already reduced the time it took to run with about 30 seconds, nice!

A example of a test suite

Let’s have a look at a test suite! Firstly the component is mounted using the “mount” method from Enzyme. This ensure the components whole lifecycle run (we want our test environment to mimic the real thing as close as possible, after all). Also in mountApp we wrap the component in the Apollo Provider-tag which allow us to run GraphQL queries and have Apollo turn the response into React props.

The waitForData and linkTestIds are helper functions to make sure we have all the nodes we’d like to test along with the API data easily accessible to the scenarios further down.

describe('#List', () => {
let app

beforeAll(async (done) => {
try {
app = await mountApp(List)
await app.waitForData('entityQuery')
app.linkTestIds()
} catch (error) {
console.error(error)
}

done()
})

Once that’s done all that’s left is to run our scenarios, for example, confirming that the rendered result corresponds to the data acquired from the API:

it('should render a list of entity teasers', () => {
expect(app.find(EntityTeaser).length).toBeGreaterThan(1)
})

it('should render teasers properly', () => {
app.find(EntityTeaser).forEach((node, index) => {
expect(node.find(Image).props().source.uri).toEqual(
normalizedData[index].imageRounded,
)

expect(getText(node.find({ testID: 'name' }))).toEqual(
normalizedData[index].name,
)

expect(getText(node.find({ testID: 'price' }))).toEqual(
`From ${normalizedData[index].price[0]}`,
)

expect(
getText(node.find({ testID: 'date' })),
).toEqual(`${normalizedData[index].date}`)
})
})

A quick word on normalization

You might have noticed the snippets above mention normalizedData. This is because of another side-effect of using a generic API; the data structure. The value you’re interested in might be nested deep down in the response, and since response will converted to a prop by Apollo which you will need to type check, test, etc. It makes sense to clean it up a bit, or in other words, normalize the response. A example of this might look like this:

const instructionEntity = (entity: any) => {
return {
size: _.get(entity, 'entity.fieldSize.entity.name'),
image: _.get(entity,
'entity.fieldMedia.entity.fieldMediaImage.derivative.url',
),

Drilling down in the response to get those values will save you a headache and a half when working with the data across your app.

In conclusion

Combining all these techs, Enzyme, Apollo, GraphQL, React Native, etc. is not trivial in itself and certainly not if you want e2e test on top. But once it clicks the reward is sweet, no more worries about your refactoring accidentally introducing regression bugs crashing the app for your clients customer. And thankfully, there’s a big, dedicated community constantly finding ways of ironing out the kinks introduced by new versions of any of the pieces involved.