A definitive guide to E2E Testing CI Setup for React-Native using Cavy-CLI

react nativefavoritesOctober 09, 2020Dotby Pulkit

image1

Preface:

React Native is a cross-platform mobile application development framework. The ability to have a common javascript codebase for multiple platforms saves development time & cost and adds the ability to ship OTA (over the air) updates.

End-to-End (E2E) testing is the methodology of spinning up your app on a real device or simulator and interacting with it as an end-user. For simplicity, this implies that a machine/robot is clicking through your app and checks whether or not a button can be clicked, a text can be typed in the search field or whatever.

The E2E Testing Framework:

Appium, Cavy & Detox are three major E2E testing frameworks to choose from. We prefer Cavy as

  • It's pure javascript based & easy to set-up
  • Compatible with continuous integration
  • Cross-Platform
  • Supports Typescript

The CI Setup

We are using CircleCI for continuous integration, though the config can easily be transpired to support other CI/CD platforms. A Getting Started guide is available as part of Cavy's official documentation with a sample CI config file available here. The problem with the sample config is it just specifies the testing setup for iOS. Further, there are no optimizations and you might end up having 40+ mins of job run-time.

A usual method to setup Android would be to spin up and use Android Docker File provided by the CircleCI, but the problem is, it would be running on Linux platform and you won't be able to share it with iOS tests. Further, common caching of node modules for both Android & iOS platform won't be possible due to different OS requirements.

Goals for an Optimized CI/CD Setup

  • We have to ensure a common OS is used to run emulators for both Android & iOS
  • Should share node_modules for both iOS & Android
  • Should cache pods for iOS and Gradle dependencies for Android
  • Should run both iOS and Android E2E Test in Parallel

Thus, In order to ensure the above-mentioned guidelines, Our CI/CD pipeline should follow the following steps

  • Step-1:
    1. Restore and Update node_modules cache
  • Step-2 (All in parallel)
    1. Run Unit Test cases (Jest)(if any)
    2. Restore and Update pods cache & Run iOS E2E Cavy Tests
    3. Restore and Update Gradle Cache & Run Android E2E Cavy Tests

Configuration Scripts

Here is how the npm-scripts look like in package.json

"scripts": {
    "test:unit-tests": "jest --verbose ./__tests__",
    "test:ios-e2e-run": "cavy run-ios -t 15",
    "test:android-e2e-run": "cavy run-android -t 15",
  }

The crucial thing here is the -t 15 flag. You might end up in a situation where the cavy-cli runs out of time while waiting to listen back from React-Native Metro Bundler. The default timeout is 3 minutes, and it's pretty easy for CI setup to take up to 20 mins to restore cache, setup emulators, boot them up & installing dependencies.

Finally here is how .circleci/config.yml looks like:

version: 2

aliases:
  - &restore-yarn-cache
    name: Restore cached root node_modules
    key: yarn-cache-{{ checksum "yarn.lock" }}
  - &restore-gradle-cache
    name: Restore cached gradle dependencies
    key: gradle_cache-{{ checksum "android/build.gradle" }}-{{ checksum  "android/app/build.gradle" }}
  - &restore-pods-cache
    name: Restore cached ios Pods
    key: pods-cache-{{ checksum "ios/Podfile.lock" }}

defaults: &defaults
  macos:
    xcode: 11.6.0

## iOS Caching
restore_pods_cache: &restore_pods_cache
  restore_cache: *restore-pods-cache

save_pods_cache: &save_pods_cache
  save_cache:
    key: pods-cache-{{ checksum "ios/Podfile.lock" }}
    paths:
      - ios/Pods

## Android Caching
restore_gradle_cache: &restore_gradle_cache
  restore_cache: *restore-gradle-cache

save_gradle_cache: &save_gradle_cache
  save_cache:
    key: gradle_cache-{{ checksum "android/build.gradle" }}-{{ checksum  "android/app/build.gradle" }}
    paths:
      - ~/.gradle

download_android_dependencies: &download_android_dependencies
  run:
    name: Download Android Dependencies
    command: |
      ls
      cd android
      ./gradlew androidDependencies
      cd ../

# shared
install_cli_interfaces: &install_cli_interfaces
  run:
    name: Install command line interfaces
    command: yarn global add cavy-cli react-native-cli

jobs:
  node:
    <<: *defaults
    parallelism: 1
    steps:
      - checkout
      - restore_cache: *restore-yarn-cache
      - run:
          name: Yarn install node modules
          command: |
            yarn install
      - save_cache:
          name: Save node_modules to cache
          key: yarn-cache-{{ checksum "yarn.lock" }}
          paths:
            - node_modules

  jest:
    <<: *defaults
    parallelism: 2
    steps:
      - checkout
      - restore_cache: *restore-yarn-cache
      - run:
          name: Run Unit Tests
          command: yarn run test:unit-tests

  ios:
    <<: *defaults
    parallelism: 2
    steps:
      - checkout
      - restore_cache: *restore-yarn-cache
      - run:
          name: Install React Native dependencies
          command: |
            brew update
            brew install watchman cocoapods || exit 0
      - *install_cli_interfaces
      - *restore_pods_cache
      - run:
          name: pods install
          command: |
            cd ios
            pod install
      - *save_pods_cache
      - run:
          name: Build app and run tests
          command: |
            rm -rf $TMPDIR/react-*
            rm -rf $TMPDIR/haste-*
            rm -rf $TMPDIR/metro-*
            watchman watch-del-all
            react-native start --reset-cache &
            yarn run test:ios-e2e-run

  android:
    <<: *defaults
    parallelism: 2
    shell: /bin/bash --login -eo pipefail
    environment:
      TERM: dumb
      JVM_OPTS: -Xmx3200m
    steps:
      - checkout
      - restore_cache: *restore-yarn-cache
      - run:
          name: Setup environment variables
          command: |
            echo 'export PATH="$PATH:/usr/local/opt/node@8/bin:${HOME}/.yarn/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin:/usr/local/share/android-sdk/tools/bin"' >> $BASH_ENV
            echo 'export ANDROID_HOME="/usr/local/share/android-sdk"' >> $BASH_ENV
            echo 'export ANDROID_SDK_HOME="/usr/local/share/android-sdk"' >> $BASH_ENV
            echo 'export ANDROID_SDK_ROOT="/usr/local/share/android-sdk"' >> $BASH_ENV
            echo 'export QEMU_AUDIO_DRV=none' >> $BASH_ENV
            echo 'export JAVA_HOME="/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home"' >> $BASH_ENV
      - run:
          name: Install Android sdk
          command: |
            HOMEBREW_NO_AUTO_UPDATE=1 brew tap homebrew/cask
            HOMEBREW_NO_AUTO_UPDATE=1 brew cask install android-sdk
            HOMEBREW_NO_AUTO_UPDATE=1 brew cask install adoptopenjdk/openjdk/adoptopenjdk8
      - run: yes | sdkmanager --licenses && sdkmanager --update || true
      - run:
          name: Install emulator dependencies
          command: (yes | sdkmanager "platform-tools" "platforms;android-26" "extras;intel;Hardware_Accelerated_Execution_Manager" "build-tools;26.0.0" "system-images;android-26;google_apis;x86" "emulator" --verbose) || true
      - *restore_gradle_cache
      - *download_android_dependencies
      - *save_gradle_cache
      - run: avdmanager create avd -n Pixel_2_API_26 -k "system-images;android-26;google_apis;x86" -g google_apis -d "Nexus 5"
      - *install_cli_interfaces
      - run:
          name: Build app and run tests
          command: |
            rm -rf $TMPDIR/react-*
            rm -rf $TMPDIR/haste-*
            rm -rf $TMPDIR/metro-*
            react-native start --reset-cache &
            yarn run test:android-e2e-run

workflows:
  version: 2
  test-suite:
    jobs:
      - node:
          filters:
            branches:
              ignore: master
      - ios:
          filters:
            branches:
              ignore: master
          requires:
            - node
      - jest:
          filters:
            branches:
              ignore: master
          requires:
            - node
      - android:
          filters:
            branches:
              ignore: master
          requires:
            - node

Results:

image2

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right