A definitive guide to E2E Testing CI Setup for React-Native using Cavy-CLI
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 andGradle 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:
- Restore and Update
node_modules
cache
- Restore and Update
- Step-2 (All in parallel)
- Run Unit Test cases (Jest)(if any)
- Restore and Update pods cache & Run iOS E2E Cavy Tests
- 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