How I Learned To Stop Worrying and Love Merging to Master

Uber ultra super rapid mobile development using React Native, Expo, Detox and CircleCI

Ori Harel
9 min readMay 21, 2019

Son, we live in a world that has walls, and those walls have to be guarded by men with guns. Who’s gonna do it? You?. — Col. Nathan R. JessepA Few Good Men (1992)”

This post is actually about well being. it’s about living a quality life as a mobile developer in 2019 (and beyond). IMHO, this means you need to:

  1. Use React Native (to enjoy Javascript and ship slick apps)
  2. Use Expo (so you won’t waste time linking and fixing libraries)
  3. Use Detox (so you can write tests and stop worry about “dangerous” merges into master)
  4. Use CI (so those tests will run all the time and pull you in if something is wrong)

I’m not going to get to the details of all of these great tools but I’ve spoiled you with links so you definitely should check them out. You take anything away from these and your life’s quality deteriorate. I kid you not.

However it’s not that easy to put all of these together. As much as React Native is gaining momentum (lots of services offer a dedicated React Native SDK), the Expo community is becoming a sub-community within React Native (and soon on even more platforms). Add to that the detox framework and a CI integration and you find yourself in no-men’s land.

But today is your lucky day. why it’s your lucky day? because I’m saving you the trouble of going through what I’ve been and walk you through the steps to accomplish the above. so lets get to it!

Create React Native App

So as mentioned, I’m talking about the fastest way to develop a mobile app, thus it must contain React Native and Expo — managed workflow. The benefit of not needing to deal with the exhaustive no-fun-at-all tasks of native libraries linking is priceless. Expo is so good that it’s the recommended way to start a project by the React Native Core Team themselves.

Write Your Code

Now it’s the time for your own app to come in. put your navigators, lists, data fetch layers, Expo SDK usage — whatever. make sure everything runs to your wishes.

Tests!

Let’s add some tests! I believe there should be 2 layers of tests:

  • Unit tests — which just test the Javascript and the React components layer. This is actually not even React Native specific. This is React in general — and that’s a good thing. This is where I’d put the majority of the tests.
  • E2E tests — which test the entire app from a user perspective. this will run the UI on a simulator, and perform UI gestures to mimic the actions of a human. Detox is perfect for this, when it comes to React Native. Writing these tests might take a little longer.

There is a great post by Josh Justice on how to set both of them up specifically for an Expo app — you should definitely check it out here. Take look at a first test I wrote:

const { reloadApp } = require('detox-expo-helpers');const isExpo = process.env.configuration === 'ios.sim.dev';describe('Example', () => {
beforeEach(async () => {
if (isExpo) {
await reloadApp();
} else {
await device.launchApp({
newInstance: true,
url: 'kahun://com.mobile.kahun?test=true'
});
}
});

it('Basic Sanity', async () => {
await expect(element(by.id('addFindingsButton'))).toBeVisible();
await element(by.id('addFindingsButton')).tap();
await expect(element(by.id('findingInput'))).toBeVisible();
await element(by.id('findingInput')).replaceText('ches');
await element(by.id('findingsScreen')).tapAtPoint({x:150, y:200});
await element(by.id('findingInput')).tap();
await element(by.id('findingInput')).replaceText('abdomi');
await element(by.id('findingsScreen')).tapAtPoint({x:150, y:200});
await element(by.id('findingInput')).tap();
await element(by.id('findingInput')).replaceText('head');
await element(by.id('findingsScreen')).tapAtPoint({x:150, y:200});
await expect(element(by.id('extendedHeader'))).toBeVisible();
await expect(element(by.id('headerGrid'))).toBeVisible();
await element(by.id('headerGrid')).tapAtPoint({x:340, y:30});
await expect(element(by.id('diagnosisItem_0'))).toBeVisible();
await element(by.id('diagnosisItem_0')).tap();
await expect(element(by.id('FindingsReasoning'))).toBeVisible();
});
});

Now run detox:

detox test

I must say at this point, that writing tests on Detox was not easy, as some commands failed to find the correct element even though I’ve assigned a testID on them. When I wasn’t able to troubleshoot the issue, I settled for the tapAtPoint() api which is not idle, but at least got me out of the jam. I’ll update this post when I’ll have better ideas.

Publish

When you’re ready with your code and tests, it’s time to publish your app for your users. This is where Expo really shines. Expo manages this entire publishing task for you (specially if you work in the managed workflow). Special note is the channel system which is very useful for testing before launch. Read all about expo channels here.

expo publish --release-channel staging

CI — Preparation

Before we integrate a CI service, we first need to do some setup. When we ran detox locally, we used the Expo binary app which loaded our local coded by calling the expo service hosted by our local machine. This could be challenging in a CI environment where our control is limited. Instead of doing that, we’re going to create our own binary app (build an Expo “standalone” app) and use that to run detox. for iOS, this means creating a standalone, simulator-mode .ipa file buy running this command:

expo build:ios -t simulator --release-channel staging

After that you’ll get a url to download the binary. Place it right next to the Exponent binary as specified in the Josh’s article.

This standalone app, will always run against the latest code published to the staging release channel of our managed app. So our workflow will be:

  • Push code to staging
  • Expo publish to staging release channel
  • Run detox with the staging standalone app

CI

Tests that don’t run automatically, on every code commit, on a CI service, don’t really exist. So in order to make meaning to our work up until now, we need to set up some CI integration.

We want our CI to:

  • Checkout our code
  • Run yarn install
  • Run the unit tests
  • Run the E2E tests
  • Publish the app to a staging channel (I leave publishing to master to your discretion. some developers like to do this step manually)

There are quite a few CI services out there, some even offer you to use Mac machines so you can run E2E tests on iOS simulator. One such service is CircleCI (although there are many more such as Bitrise, Microsoft’s AppCenter and perhaps more that I’m not aware of). I chose CircleCI because of an already existing integration in my company.

CircleCI integration includes, among other things like creating the account, adding a file called config.yaml to be placed under .circleci folder at the root of your project. Take look at my config.yaml:

version: 2
jobs:
expo-publish-staging:
working_directory: ~/kne.mobile
docker:
- image: circleci/node:8
steps:
- checkout

- run:
name: Installing dependencies
command: yarn install

- run:
name: Login into Expo
command: npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD

- run:
name: Publish to STAGING
command: npx expo publish --release-channel staging
e2e-test:
macos:
xcode: "10.2.1"

steps:
- checkout
- restore_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}

- restore_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}

- run: yarn install

- save_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
paths:
- ~/.cache/yarn

- save_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules

- run:
name: Install applesimutils
command: |
brew tap wix/brew
brew install applesimutils

- run:
name: Install react-native and detox CLI
command: |
npm install -g react-native-cli
npm install -g detox-cli

- run:
name: Prepare detox env
command: |
detox clean-framework-cache && detox build-framework-cache

- run:
name: Run detox test
command: yarn detox-stag
workflows:
version: 2
e2e-workflow:
jobs:
- expo-publish-staging:
filters:
branches:
only: staging
- e2e-test
requires:
expo-publish-staging
filters:
branches:
only: staging

Let’s go over this one by one:

version: 2
jobs:
expo-publish-staging:
working_directory: ~/someDir
docker:
- image: circleci/node:8
steps:
- checkout

It all starts with indicating the version of CircleCI and declaring the job, named expo-publish-staging (remember that this is run after code was pushed to staging). Then we setup the working directory, specify the docker machine (I just went with the defaults here, just follow CircleCI docs). so the first step we do is to checkout the code.

      - run:
name: Installing dependencies
command: yarn install

- run:
name: Login into Expo
command: npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD

- run:
name: Publish to STAGING
command: npx expo publish --release-channel staging

The next 3 steps are 3 run commands: installing dependancies, login into expo and perform the publish command.

The next job will be the e2e-test. This is the job where we run the detox test on an iOS simulator, so we indicate that we want to use a macos machine in order to run this job. CircleCI offers other machines such as Linux or a Docker image. Note — in order to run a Mac machine on CircleCI you need to have a paid plan (you’ll get 2 weeks free without credit card — cool!).

version: 2
jobs:
e2e-test:
macos:
xcode: "10.2.1"

Then comes these steps: checkout, restore_cache, run:yarn install, save_cache. These are steps that are all about checking out the code from the repository and installing (and caching) dependancies.

Now, for some detox setup steps:

- run:
name: Install applesimutils
command: |
brew tap wix/brew
brew install applesimutils

- run:
name: Install react-native and detox CLI
command: |
npm install -g react-native-cli
npm install -g detox-cli

- run:
name: Prepare detox env
command: |
detox clean-framework-cache && detox build-framework-cache

These are covered in detox getting started docs.

And now comes our part:

- run:
name: Run detox test
command: yarn detox-stag

This is where we call our package.json script:

"scripts": {
...
"detox-dev": "detox test -c ios.sim.dev",
"detox-stag": "detox test -c ios.sim.staging",
...
},

As you can see, I run detox-stag in my CI (and detox-dev locally). Let’s look at my detox configuration in package.json:

"detox": {
"test-runner": "jest",
"configurations": {
"ios.sim.dev": {
"binaryPath": "bin/Exponent.app",
"type": "ios.simulator",
"name": "iPhone 8"
},
"ios.sim.staging": {
"binaryPath": "binaryApp/staging.app",
"type": "ios.simulator",
"name": "iPhone 8"
}
}
},

Using the -c switch of detox-cli, I distinguish between running detox locally and running in CI. When running locally, I point the configuration to the Exponent.app binary, so it’ll load my local Javascript code. This is how I write the tests and check them. But in order for this to work, I need to fire up a local Expo server

expo start

As mentioned, this is not so trivial in a CI environment (at least to my understanding — if you have any ideas how to run this I’d be happy to hear). So this is why in the CI, I’m using thebuilt-up binary (the staging.app) that I’ve “manually” built.

The end of the config.yaml is just declaring the workflow and the jobs as a childs:

workflows:
version: 2
e2e-workflow:
jobs:
- expo-publish-staging:
filters:
branches:
only: staging
- e2e-test
requires:
expo-publish-staging
filters:
branches:
only: staging

Note that we create a dependancy to run e2e-test only after expo-publish-staging is completed successfully.

And that’s it!! you have an E2E detox running in a CI server. how cool is that??

From here you can play with the various configuration that CircleCI provides (such as adding a scheduled job that runs this every hour and not just when code is committed).

If your stack is different, say you have a regular React Native app, or you’ve ejected from Expo (the “Bare” workflow) then the tutorials in detox and CircleCI docs should have you covered.

Summary

So we managed to have a safe guard that keeps us safe from merging and pushing our code. Again the clarification though — as I mentioned that writing detox tests could be time consuming and tricky, I don’t expect to write many tests with detox. Most of your tests should be closer to unit tests, which — as in Josh’s article — are covered by react-native-testing-libraray and Jest. Those can be run very easily in an CI services and are not covered in this post (there are plenty around the internet).

Good Luck!

--

--

Ori Harel

Engineering Manager, love startups, love NBA Basketball but mostly procrastinate. Work @ Taranis