A Mostly Sensible Account of Testing a React Native App

James Munro
8 min readJan 30, 2017

--

I recently shipped an app written in React Native which took advantage of automated testing. In this article I’d like to explore the approach I took in the hope that it might be useful for your own app projects. It’s worth mentioning from the start that there are many different approaches and there’s no one-size-fits-all solution. Although I don’t claim to have any ground-breaking new strategies, I’d like to share my recent experiences.

It Started With a Bug

I began my journey into automated testing strategy after learning about its importance the hard way. Back in 2011 I was working for a mobile game developer on our own in-house products. At this stage in my career, I was interested in the idea of automated testing, however I’d never really successfully applied it to a real-world project. During some routine, well-intended refactoring work, I inadvertently introduced a bug into a game which essentially broke the core gameplay mechanic and unfortunately this was not picked up during our manual quality assurance process. From this day forward my approach to developing apps pivoted and I’ve never looked back.

Finding a Balance

I no longer work in the games industry, and in some ways this made it easier for me to embrace a quality-first mentality towards software development due to the mostly less ‘fuzzy’ nature of the kinds of business logic we implement.

For a while, I took things too far with a relentless test everything ’til you drop approach. One of the biggest lessons I’ve learned so far in my career is that there isn’t a one-size-fits-all solution for anything in software development. I’ve worked on projects that were rigorously tested and developed from scratch with extensibility and maintainability in mind, only for the project to receive a smattering of incremental updates over the period of a few years.

On the other side of the coin, I’ve also been involved with projects that were only ever intended as a proof-of-concept and so had little investment in terms of longevity and maintainability — very little in terms of automated testing — only for these projects to be scaled up at short notice into fully-fledged products. Speaking from experience, it is very difficult to retroactively introduce automated testing to a project that wasn’t designed with testability in mind.

This begs the question of how do you know how rigorous to be with your testing strategy? There are many factors involved, ranging from the life expectancy of the project to the budget and resources available to develop it. The simple answer is that there’s no right or wrong answer to this, however, there are sensible strategies that we can adopt and scale up or down as necessary.

The Challenge

For fun and to keep my skills sharp I developed a cross-platform app, Meal Shuffle, in my spare time using React Native. During this project I’ve often struggled with determining the right level of automated testing to apply, but I’ve found a happy balance that worked for me.

I’m a big fan of TDD (test-driven development), and in a world that is embracing functional programming and purity, it is easier than ever to apply this approach to development. However, I think it’s very important to validate your ideas before you embark down this path. TDD generally works best when you already know what you’re developing, and in the early stages of a new project, we don’t always have the answers.

Before I jumped into developing any real business logic I did my best to validate my ideas by creating some prototypes. React Native provides a fantastic developer experience for this and its extremely quick and convenient to throw a barebones app together in a short space of time.

The app I wanted to develop is just a simple tool designed to help busy people with their meal planning. There are plenty of other apps out there that do this kind of thing but none of them worked in the way I wanted. I prototyped out some of the main screens of the app in order to begin collecting feedback and validating my assumptions.

An early prototype. Clearly not going to win any design awards!

I spent some time collecting feedback from friends and family on the actual functionality of the app and then threw all of the code away.

There were various bits and pieces of functionality that I could have spent ages writing tests for, but the only code that matters is code that ships, and lots of ideas I’d had simply weren’t as useful to the end-user as I’d initially expected.

Prototype code is not production code and shouldn’t be bullied into being it.

With my ideas validated I was ready to begin the proper development of the app.

Starting Over

Now that I understood what I wanted to build it was easier for me to start building out the logic using a TDD approach: writing the tests first, followed by the code to pass the tests.

Adopting this approach is something I’m still very happy about. I mentioned earlier that this app is just a side-project around my full-time job and as a result, sometimes weeks can go by when I don’t spend any time on the project. Having the tests as documentation makes it much easier for me to dip in and out of the project. I guarantee that as time passes you forget not only how things work, but also what the code is supposed to do in the first place.

Being able to pull up the test file and understand what a past version of myself intended is one of the many benefits of having a good suite of tests around your project.

An example unit test from Meal Shuffle — helpful documentation!

Choosing the right level of depth for your tests is another fuzzy thing that is hard to create a rule for. I would certainly recommend it for the vast majority of the business logic of your app. The real key is to have enough tests in place that you can move fast with the development of new features.

A test-first approach to writing the business logic of my app was good enough to get me to a 1.0 release. I could have gone a step further and layered in additional levels of automated testing, such as automated UI tests. These can be great because they allow you to perform end-to-end tests of features through UI interactions. However, although my idea had been validated against friends and family it was not yet in the hands of any real-world users.

Automated UI tests have their advantages, however, they can also be quite time-consuming to put together and can be flaky, and I was not yet ready to invest in them for my app.

Snapshot Testing

Over the last year we’ve seen a great increase in the ease at which we can write test for our React Native applications. Amazing tools like Jest have seen significant investments in development by companies such as Facebook and this brought new testing techniques to the forefront.

One such example of this is snapshot testing. A great use of snapshot testing enables us to capture the state of a UI component written in React or React Native and serialize this to a file. This captured snapshot test now essentially serves as a regression test which will flag up any changes that occur to output and allows you to inspect whether or not this was an intentional change.

With the first version on my app on the store and my ideas now validated in the hands of real end-users I decided to explore the potential of snapshot testing to protect myself from regressions to my app’s UI. This has proven to be particularly fruitful in a React Native application: although relatively stable the framework itself is under constant development and has a version of less than 1.0. Breaking changes are becoming less frequent but recently there have been a few updates to the Flexbox handling which have caused my UI to appear differently at runtime depending on the version used.

One of the great things about snapshot testing is that we can exploit the tree-based nature of React-based UIs: you can go crazy by creating snapshots for each individual component of your app, or you can exploit the tree-based nature and target a root component such as a screen. Capturing the a snapshot of the root of a tree will also capture all of the leaf components, essentially allowing you to capture a top-down representation of the entire UI depending on the granularity to select.

In my app I decided to capture snapshots of the main screens of the app to help protect against regressions:

An example snapshot test from Meal Shuffle

This has been super useful in catching unintentional changes and regressions during continued development:

Unintentional changes — caught by a snapshot test

With snapshot tests being so simple to write and by exploiting the tree-based nature of React-based UIs, you can get some really quick-wins. I highly encourage giving this a go in your app, even if you decide not to use TDD for your business logic.

Moving Forwards

Overall I’m very happy with the degree at which I decided to bolster my app project with automated tests. Over a couple of months, I’ve ended up shipping multiple updates and having a suite of automated tests has saved me from shipping bugs on several occasions due to refactoring gone wrong. Not only that, due to the side-project status and limited time I get to spend on the app, having a detailed documentation of how, what and why everything works has allowed me to dip in and out of the project as time permits.

Meal Shuffle 1.0, supported with automated testing

The recent introduction of snapshot testing to Jest have allowed me to further protect myself from regressions, often caused by my own stupidity, at the UI level. This has proven to be particularly helpful whilst attempting to build apps on React Native, which is still under constant development.

Choosing the right level of testing for your project is not an easy question to answer. It’s not easy to do TDD when you don’t already know what you’re trying to build. However, throwaway prototypes can be a cheap way for you to validate your ideas before building a more robust version of your application.

In 2017, there’s such a great level of tool support for automated testing that there are lots of quick wins to be had, even if you don’t have a huge amount of development resource to invest. TDD isn’t for everyone and isn’t always the best solution. Automated UI tests can be great for end-to-end testing but can require a lot of proactive maintenance which can be especially difficult on a project that is constantly evolving. Snapshot tests strike a balance between the 2: they can be setup in just a few lines of code and can protect the UI from regressions, however they don’t help a great deal in doing the same for your business logic.

What kind of testing strategies have you applied in your app projects?

--

--