DEV Community

Roberto Junior Amarante Calderón
Roberto Junior Amarante Calderón

Posted on • Updated on

React Native e2e tests and Automatic Deploys (Detox + Fastlane + CircleCI)

A little bit of context (skippable)

These past weeks I've been struggling trying to set up a pipeline where for every PR pointing to our staging branch, e2e tests run automatically, and for every PR that gets merged, test flight builds, and google internal beta builds are created. My experience has been... difficult, but it doesn't mean yours should too.

tl;dr; PAIN.

What do I need for this?

1. Circle CI's performance plan.

Since we are going to use macs for building our apps. If you only need android builds, you can easily achieve this with github actions, seethis project for an example and this amazing post.

2. Apple connect account & Google play console account.

This will be needed for automatic deployments(2nd part).

3. Patience

Trust me, you'll need it. CIs can smell fear.

Let's get this started

1. Add detox into your react native project.

Please, follow this guide using JEST step by step in order to have it correctly configured in your project. Here is anexample of a .detoxrc.json.

Once you can run e2e tests locally, you're ready to go for the next step.

2. Set up CircleCI.

If you don't have CircleCI, you can learn how to add it here.
Don't worry too much about the content on the config.yml, since we are going to completely modify it. :)

At this point, you should have a folder named .circleci/ with a config.yml file.

Let's make magic using orbs!

Add this to your /app/build.gradle since we are going to use the react-native-circleci-orb.

task downloadDependencies() {
  description 'Download all dependencies to the Gradle cache'
  doLast {
    configurations.findAll().each { config ->
      if (config.name.contains("minReactNative") && config.canBeResolved) {
        print config.name
        print '\n'
        config.files
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Feeling lucky?

React native community example of how to use this orb is the following:

version: 2.1

orbs:
  rn: react-native-community/react-native@5.1.0
# Custom jobs which are not part of the Orb
jobs:
  checkout_code:
    executor: rn/linux_js
    steps:
      - checkout
      - persist_to_workspace:
          root: .
          paths: .
  analyse_js:
    executor: rn/linux_js
    steps:
      - attach_workspace:
          at: .
      - rn/yarn_install
      - run:
          name: Run ESLint
          command: yarn eslint
      - run:
          name: Flow
          command: yarn flow
      - run:
          name: Jest
          command: yarn jest

workflows:
  test:
    jobs:
      # Checkout the code and persist to the Workspace
      # Note: This is a job that is defined above and not part of the Orb
      - checkout_code

      # Analyze the Javascript using ESLint, Flow, and Jest
      # Note: This is a job that is defined above and not part of the Orb
      - analyse_js:
          requires:
            - checkout_code

      # Build the Android app in debug mode
      - rn/android_build:
          name: build_android_debug
          project_path: "android"
          build_type: debug
          requires:
            - analyse_js

      # Build and test the Android app in release mode
      # Note: We split these into separate jobs because we can build the Android app on a Linux machine and preserve the expensive MacOS executor minutes for when it's required
      - rn/android_build:
          name: build_android_release
          project_path: "android"
          build_type: release
          requires:
            - analyse_js
      - rn/android_test:
          detox_configuration: "android.emu.release"
          requires:
            - build_android_release

      # Build the iOS app in release mode and do not run tests
      - rn/ios_build:
          name: build_ios_release
          project_path: ios/Example.xcodeproj
          device: "iPhone X"
          build_configuration: Release
          scheme: Example
          requires:
            - analyse_js

      # Build and test the iOS app in release mode
      - rn/ios_build_and_test:
          project_path: "ios/Example.xcodeproj"
          device: "iPhone X"
          build_configuration: "Release"
          scheme: "Example"
          detox_configuration: "ios.sim.release"
          requires:
            - analyse_js
Enter fullscreen mode Exit fullscreen mode

But there is a catch, in my experience, it did not work. Here are the docs of every helper function on this orb.


What's next?

Welp let's go step by step and create something that works ;)

Orb

version: 2.1
orbs:
  rn: react-native-community/react-native@5.1.0
Enter fullscreen mode Exit fullscreen mode

Note that we call it rn, this name can be whatever you want, and it's just used to specify when a job is coming from the orb. Ex. rn/yarn_install

Jobs

checkout_code

Check out the code and persist to the Workspace, needed in order to do stuff in the project root.

checkout_code:
  executor:
    name: rn/linux_js
    node_version: "12"
  steps:
    - checkout
    - persist_to_workspace:
        paths: .
        root: .
Enter fullscreen mode Exit fullscreen mode
analyse_js

Running jest test on Linux. Note how we use an executor from our orb and define the node_version version for our project.

analyse_js:
  executor:
    name: rn/linux_js
    node_version: "12"
  steps:
    - attach_workspace:
        at: .
    - rn/yarn_install
    - run:
        command: yarn test
        name: Run Tests
Enter fullscreen mode Exit fullscreen mode
Android e2e

In a perfect world, the example on the docs is all you need. But this is programming, specifically, React native that we're talking about, the example is the following:

- rn/android_build:
    build_type: debug
    name: build_android_debug
    project_path: android
    requires:
      - analyse_js
- rn/android_build:
    build_type: release
    name: build_android_release
    project_path: android
    requires:
      - analyse_js
Enter fullscreen mode Exit fullscreen mode

The main issue with this approach is that rn/android_build builds the app as a normal build and not as a detox build which can lead to weird issues and false-negative e2e tests.

So... yeah, we have to re-do this step manually, but feel free to try! If it works for you, shame me on Twitter!.

Please read the comments to understand what is going on here.

android_e2e_test:
  # Using a mac (:
  executor:
    name: rn/macos
  steps:
    - attach_workspace:
        at: .
    - rn/setup_macos_executor:
        homebrew_cache: true
        node_version: "12"
    - rn/yarn_install:
        # basically because of this https://github.com/react-native-community/react-native-circleci-orb/issues/66
        cache: false
    - run:
        # For my app and react native in general java8 is needed. The default version on this executor was default to java10 for some reason, so this kinda solve that issue.
        # just installing java, android sdk, and needed tools.
        command: >
          java -version

          brew tap adoptopenjdk/openjdk

          brew install --cask adoptopenjdk/openjdk/adoptopenjdk8

          java -version

          export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

          mkdir -p ~/.android && touch ~/.android/repositories.cfg

          java -version

          yes | sdkmanager "platform-tools" "tools" >/dev/null

          yes | sdkmanager "platforms;android-29"
          "system-images;android-29;default;x86_64" >/dev/null

          yes | sdkmanager "emulator" --channel=3 >/dev/null

          yes | sdkmanager "build-tools;29.0.2" >/dev/null

          yes | sdkmanager --licenses >/dev/null

          yes | sdkmanager --list
        name: Install Android Emulator
        shell: /bin/bash -e
    - run:
        command: |
          adb start-server
          adb devices
          adb kill-server
          ls -la ~/.android
        name: ADB Start Stop
    - run:
        # Note we are using a pixel_xl as the test device, feel free to change it for one better fits your app
        command: |
          export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
          avdmanager create avd --force --name Pixel_2_API_29 --package "system-images;android-29;default;x86_64" --tag default --device pixel_xl
        name: Create Android Emulator
    - run:
        background: true
        command: |
          export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
          $ANDROID_HOME/emulator/emulator @Pixel_2_API_29 -version
          $ANDROID_HOME/emulator/emulator @Pixel_2_API_29 -cores 2 -gpu auto
          -accel on -memory 2048 -no-audio -no-snapshot -no-boot-anim
          -no-window -logcat *:W | grep -i
          'ReactNative\|com.reactnativecommunity'
        name: Start Android Emulator (background)
    - run:
        command: >
          # export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

          export BOOT=""

          echo "Waiting for AVD to finish booting"

          export PATH=$(dirname $(dirname $(command -v
          android)))/platform-tools:$PATH

          until [[ "$BOOT" =~ "1" ]]; do
            sleep 5
            export BOOT=$(adb -e shell getprop sys.boot_completed 2>&1)
          done

          sleep 15

          adb shell settings put global window_animation_scale 0

          adb shell settings put global transition_animation_scale 0

          adb shell settings put global animator_duration_scale 0

          echo "Android Virtual Device is now ready."
        name: Wait for AVD to be ready
        no_output_timeout: 5m
    # Creates the detox build using the orb job
    - rn/detox_build:
        configuration: "android.emu.release"
    # Tests the app, you can use rn/detox_test, but I wanted to take screenshots when test fails so I can have a better idea of why did they fail.
    - run:
        command: >-
          detox test -c android.emu.release -l warn --headless
          --take-screenshots failing --artifacts-location /tmp/detox_artifacts
        name: Detox Test
    # Save the screenshots as artifacts, you can see then in the artifact tab for the job in CircleCI
    - store_artifacts:
        path: /tmp/detox_artifacts
Enter fullscreen mode Exit fullscreen mode

Note that all of this can be achieved using the rn/linux_android executor.

iOS e2e

In a perfect world, the example on the docs is all you need. And it was for me... until it wasn't. Try the following, if that works for you, shame me on Twitter!.

# Build and test the iOS app in release mode
- rn/ios_build_and_test:
    project_path: "ios/Example.xcodeproj"
    device: "iPhone X"
    build_configuration: "Release"
    scheme: "Example"
    detox_configuration: "ios.sim.release"
    requires:
      - analyse_js
Enter fullscreen mode Exit fullscreen mode

Fortunately, ios is better than android. Yeah, I said it... At least development wise. In order to recreate the ios_build_and_test all we need is:

# Build and test the iOS app in release mode
ios_e2e_test:
  executor: rn/macos
  steps:
    - checkout
    - attach_workspace:
        at: .
    - rn/setup_macos_executor:
        homebrew_cache: true
        node_version: "12"
    - rn/ios_simulator_start:
        device: "iPhone 11"
    - rn/yarn_install:
        # basically because of this https://github.com/react-native-community/react-native-circleci-orb/issues/66
        cache: false
    - rn/pod_install:
        pod_install_directory: ios
    # Yep, it doesn't really matter if you don't run detox build for ios, it works like a charm. But if you prefer, you can replace this step with a custom one.
    - rn/ios_build:
        build_configuration: "Release"
        cache: false
        derived_data_path: "ios/build"
        device: "iPhone 11"
        project_path: "ios/example.xcworkspace"
        project_type: workspace
        scheme: "example"
    - run:
        command: >-
          detox test -c ios.sim.release -l warn --headless --take-screenshots
          failing --artifacts-location /tmp/detox_artifacts
        name: Detox Test
    - store_artifacts:
        path: /tmp/detox_artifacts
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have e2e tests running in your app! Give yourself a pat in the back and go get a drink, because Fastlane is coming.

The hardest thing is doing the configurations for your project. Feel free to ask in the comments, but fastlane documentation should be enough to get you ready for the next steps.

Checkout these if you need a place to start:

Alt Text

Fastlane android

This is easier than what you already did. :) All we need is to install Fastlane on Linux and run our Fastlane lane.

fastlane_android_internal:
  executor: rn/linux_android
  steps:
    - attach_workspace:
        at: .
    - rn/yarn_install
    - run:
        command: gem install bundler
        name: Install bundler
    - run:
        command: gem install fastlane
        name: Install Fastlane
    # Note that my lane is name upload_to_googleplay replaced for yours
    - run:
        # can be fancier and use working_directory
        command: cd android && fastlane upload_to_googleplay
        name: Upload to google play via Fastlane
Enter fullscreen mode Exit fullscreen mode
Fastlane ios

I'm pretty sure adding Fastlane to ios was not an easy task. So... Congratulations Shinji! These are basically the same steps but for ios.

# submit app to apple connect testflight
fastlane_ios_testflight:
  executor:
    name: rn/macos
  steps:
    - attach_workspace:
        at: .
    - rn/yarn_install:
        cache: false
    - run:
        working_directory: ios
        command: pod install
    - run:
        command: gem install bundler
        name: Install bundler
    - run:
        command: gem install fastlane
        name: Install Fastlane
    - run:
        working_directory: ios
        command: fastlane beta
        name: Upload to Testflight via Fastlane
Enter fullscreen mode Exit fullscreen mode

So... tips for Fastlane.

  • Fastlane Docs.
  • 2fa for apple connect.
  • CircleCI Docs.
  • Use date for build numbers. (There are other ways to get incremental build numbers, if you want to try them, Can't recommend any since I haven't used any for the ci).
    • android: in the build.gradle (int)(date.getTime() / 10000)
    • ios: in fastlane/Fastfile build_number: DateTime.now.strftime("%Y%m%d%H%M")

One more thing

In order to make everything work, we need to create a workflow where we define the order of the steps.

So... here's a proposal:

workflows:
  # name of the workflow
  main:
    jobs:
      - checkout_code
      # Do jest tests
      - analyse_js:
          requires:
            - checkout_code
      # Build and test the android app in release mode
      - android_e2e_test:
          requires:
            - analyse_js
      # Build and test the iOS app in release mode
      - ios_e2e_test:
          requires:
            - analyse_js
      # Release apps to stores for testing
      - fastlane_android_internal:
          # We only want to deploy to google play when things get merged into the main branch
          filters:
            branches:
              only:
                - main
          # Note that e2e need to pass in order to release
          requires:
            - android_e2e_test
      - fastlane_ios_testflight:
          # We only want to deploy to google play when things get merged into the main branch
          filters:
            branches:
              only:
                - main
          # Note that e2e need to pass in order to release
          requires:
            - ios_e2e_test
Enter fullscreen mode Exit fullscreen mode

If react native, detox, CircleCI and Fastlane decided you can rest today, you should see something like this in your pipeline.

Alt Text

Latest comments (11)

Collapse
 
krishnag09 profile image
Krishna Gaurav

Thanks for the post, very well detailed.
Could i ask what was the build time for this rn/ios_build step? And if you have thought of ways to reduce it?

Collapse
 
kyonru profile image
Roberto Junior Amarante Calderón

Yeah.... Mayor update, you can get the same result with:

version: 2.1

orbs:
  rn: react-native-community/react-native@5.5.0

jobs:
  checkout_code:
    executor:
      name: rn/linux_js
      node_version: '12'
    steps:
      - checkout
      - persist_to_workspace:
          paths: .
          root: .
  analyse_js:
    executor:
      name: rn/linux_js
      node_version: '12'
    steps:
      - attach_workspace:
          at: .
      - rn/yarn_install
      - run:
          command: yarn lint
          name: Run Linter
      - run:
          command: yarn jest
          name: Run Tests

  # submit app to playstore for internal test
  fastlane_android_internal:
    executor: rn/linux_android
    steps:
      - attach_workspace:
          at: .
      - rn/yarn_install
      - run:
          command: bash create_staging_env_files.sh
          name: Create env files
      - run:
          command: cat .env
          name: Print env files
      - run:
          command: gem install bundler
          name: Install bundler
      - run:
          command: gem install fastlane
          name: Install Fastlane
      - run:
          command: cd android && fastlane googleplay
          name: Upload to google play via Fastlane
  # submit app to apple connect testflight
  fastlane_ios_testflight:
    executor:
      name: rn/macos
    steps:
      - attach_workspace:
          at: .
      - rn/yarn_install:
          cache: false
      - run:
          command: bash create_staging_env_files.sh
          name: Create env files
      - run:
          command: cat .env
          name: Print env files
      - run:
          working_directory: ios
          command: pod install
      - run:
          command: gem install bundler
          name: Install bundler
      - run:
          command: gem install fastlane
          name: Install Fastlane
      - run:
          command:
            git config --global --add url."git@github.com:".insteadOf
            "https://github.com/"
          name: Use SSH
      - run:
          working_directory: ios
          command: fastlane beta
          env:
            MATCH_GIT_BASIC_AUTHORIZATION: $MATCH_GIT_BASIC_AUTHORIZATION
          name: Upload to Testflight via Fastlane

  # submit app to playstore for beta, ready to release to prod
  fastlane_android_beta:
    executor: rn/linux_android
    steps:
      - attach_workspace:
          at: .
      - rn/yarn_install
      - run:
          command: bash create_prod_env_files.sh
          name: Create env files
      - run:
          command: cat .env
          name: Print env files
      - run:
          command: gem install bundler
          name: Install bundler
      - run:
          command: gem install fastlane
          name: Install Fastlane
      - run:
          command: cd android && fastlane googleplaymanualprod
          name: Upload to google play via Fastlane
  # submit app to apple connect ready for review
  fastlane_ios_app_store:
    executor:
      name: rn/macos
    steps:
      - attach_workspace:
          at: .
      - rn/yarn_install:
          cache: false
      - run:
          command: bash create_prod_env_files.sh
          name: Create env files
      - run:
          command: cat .env
          name: Print env files
      - run:
          working_directory: ios
          command: pod install
      - run:
          command: gem install bundler
          name: Install bundler
      - run:
          command: gem install fastlane
          name: Install Fastlane
      - run:
          command:
            git config --global --add url."git@github.com:".insteadOf
            "https://github.com/"
          name: Use SSH
      - run:
          working_directory: ios
          command: fastlane prod
          env:
            MATCH_GIT_BASIC_AUTHORIZATION: $MATCH_GIT_BASIC_AUTHORIZATION
            SENTRY_AUTH_TOKEN: $SENTRY_AUTH_TOKEN
            ASCAPI_KEY_ID: $ASCAPI_KEY_ID
            ASCAPI_ISSUER_ID: $ASCAPI_ISSUER_ID
            ASCAPI_KEY_CONTENT: $ASCAPI_KEY_CONTENT
          name: Upload to Testflight via Fastlane
workflows:
  test:
    jobs:
      - checkout_code
      - analyse_js:
          requires:
            - checkout_code
      - rn/android_build:
          name: build_android_release
          project_path: 'android'
          build_type: release
          on_after_initialize: |
            bash create_env_files.sh
          requires:
            - analyse_js
      - rn/android_test:
          name: android_e2e_test
          detox_configuration:
            'android.emu.release --take-screenshots failing --artifacts-location
            /tmp/detox_artifacts --cleanup --record-logs failing'
          device_name: Pixel_2_API_29
          platform_version: android-29
          build_tools_version: '29.0.3'
          yarn_cache: false
          requires:
            - build_android_release
          detox_loglevel: 'verbose'
          store_artifact_path: '/tmp/detox_artifacts'
          should_on_after_initialize: true
          on_after_initialize: |
            HOMEBREW_NO_AUTO_UPDATE=1 brew tap adoptopenjdk/openjdk
            HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask adoptopenjdk/openjdk/adoptopenjdk8
            echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)' >> $BASH_ENV

        # Build and test the iOS app in release mode
      - rn/ios_build_and_test:
          name: ios_e2e_test
          checkout: true
          project_path: 'ios/mobileApp.xcworkspace'
          project_type: workspace
          device: 'iPhone 11'
          pod_install_directory: ios
          on_after_initialize: |
            bash create_env_files.sh
          build_configuration: 'Release'
          scheme: 'appScheme'
          detox_configuration:
            'ios.sim.release --take-screenshots failing --artifacts-location
            /tmp/detox_artifacts --cleanup --record-logs failing'
          detox_loglevel: 'verbose'
          store_artifact_path: '/tmp/detox_artifacts'
          yarn_cache: false
          xcodebuild_cache: false
          requires:
            - analyse_js
      # Release apps to stores for testing
      - fastlane_android_internal:
          filters:
            branches:
              only:
                - staging
          requires:
            - android_e2e_test
      - fastlane_ios_testflight:
          filters:
            branches:
              only:
                - staging
          requires:
            - ios_e2e_test
      # Release apps to stores for release [manual]
      - fastlane_android_beta:
          filters:
            branches:
              only:
                - main
          requires:
            - android_e2e_test
      - fastlane_ios_app_store:
          filters:
            branches:
              only:
                - main
          requires:
            - ios_e2e_test

Enter fullscreen mode Exit fullscreen mode
Collapse
 
kyonru profile image
Roberto Junior Amarante Calderón

I'll make sure to update this post when I get some time ToT

Collapse
 
kierano547 profile image
Kieran Osgood • Edited

When you chose the macos executor instead of the linux_android for the android e2e tests: "Note that all of this can be achieved using the rn/linux_android executor." was there any reason for that choice? The macos minutes are more expensive AFAIK, and you chose to mention both would work, so just curious if it was more difficult to do or something?

p.s. I found that the rn/ios_build_and_test command worked just fine when used like this:

      - rn/ios_build_and_test:
          yarn_cache: false
          xcodebuild_cache: false
          homebrew_cache: false
          project_type: "workspace"
          project_path: "ios/punchline.xcworkspace"
          device: "iPhone 11"
          build_configuration: "Release"
          scheme: "punchline"
          detox_configuration: "ios.sim.release"
          requires:
            - checkout_code
            - analyse_js
            - update_homebrew
Enter fullscreen mode Exit fullscreen mode

Currently trying to decide how I can most efficiently configure the e2e tests on release, and ideally use the same build artifacts from that to release through fastlane or something

Collapse
 
krishnag09 profile image
Krishna Gaurav

@ Kieran Osgood - Were you able to find a way to reduce the ios build time for detox, using macos or linux as env?

Collapse
 
kyonru profile image
Roberto Junior Amarante Calderón

That definitely would save some time. I'm not sure of the implications of publishing the detox build for android. But It would definitely help for ios.

Collapse
 
kyonru profile image
Roberto Junior Amarante Calderón

In the beginning, I tried with the linux_executor. I don't remember exactly why I changed, I think it was related to all the detox dependencies and setting up the environment. (But later other issues came up, so I had to do all of that... At this point, it should be that hard to change the executor, but idk since I need to try it again).

Thanks for the suggestion! I'll make an update with your suggestion with iOS.

Collapse
 
acro5piano profile image
Kay Gosho

Thanks for the great article! Do you have any experience in GitHub actions? I tried it before and not works as well as CircleCI.

Collapse
 
kyonru profile image
Roberto Junior Amarante Calderón

Sadly I have just use github actions to test detox, but I'm planning on doing this workflow sooner than later for this app in github actions. github.com/Kyonru/just-a-review-app

Collapse
 
acro5piano profile image
Kay Gosho • Edited

Thanks! I think maybe there is a problem running iOS build on GitHub actions, whereas CircleCI works much better thanks to the ORB. Looking forward to the GitHub Actions version!

Collapse
 
blashadow_62 profile image
Luis

sounds good to automate some react-native dev-ops things, also reading the documentation from react-native-circleci-orb really makes sure you know how hard is what you're about to do.