Alright, if you’re building a React Native app without Expo EAS, or if you prefer the flexibility of your own custom setup rather than relying on managed services like Maestro Cloud for test orchestration, setting up proper end-to-end (E2E) testing on CircleCI can be challenging. While many apps are moving towards the full Expo ecosystem. Given that it’s the officially recommended framework from the React Native team, there are still plenty of projects using custom build systems and standalone frameworks, where having fine-grained control over your CI/CD pipeline is crucial.
I’ve often found myself building custom setups for E2E testing because I like keeping things under my control – without relying too much on managed services. This approach keeps things flexible, scalable, and cost-effective, but it also means you need to get the details right. If you’re in the same boat, this guide is for you.
The Gist of It
If you’re ready to jump right into the code, check out the Gist on GitHub – a complete, ready-to-run config.yml
for running Maestro tests on CircleCI. It includes:
- Reusable test command (
run-maestro-tests
) - iOS and Android job configurations
- Best practices for stable test runs on virtualized CI environments
Fork it, tweak it, and drop a star if it saves you some headaches.
Breaking Down the config.yml
Here’s a quick breakdown of the key sections:
1. Orbs
I have used the CircleCI Android orb to simplify emulator setup and build:
orbs:
android: circleci/[email protected]
2. Execution Environment
Using the macOS execution environment for iOS, which you can read more about here:
macos:
xcode: 16.0.0
3. The run-maestro-tests
Command
I kept this command reusable so you don’t have to rewrite it for iOS and Android:
commands:
run-maestro-tests:
description: 'Run Maestro Tests'
parameters:
platform:
type: enum
enum: [android, ios]
description: 'Platform to run the tests for'
maestro_file:
type: string
description: 'Path to the Maestro test file'
steps:
- run:
name: Run Maestro Tests for << parameters.platform >>
command: |
maestro test << parameters.maestro_file >>
if [ $? -eq 0 ]; then
echo "✅ Tests Passed"
else
echo "❌ Tests Failed"
fi
4. Android Job
Choosing the right resource_class
is critical. You don’t want your builds timing out or failing because you ran out of memory. Check out the CircleCI resource classes for a good overview.
run_maestro_android:
executor:
name: android/android_machine
resource_class: xlarge # 8GB / 16GB for better stability
steps:
- checkout
- install_node_modules
- install_gems
- install_maestro_cli
- android/create_avd:
avd_name: test
system_image: 'system-images;android-34;google_apis;x86_64'
install: true
additional_args: '--device pixel_4'
- run:
name: Build Android App
command: |
cd android
./gradlew assembleRelease --no-daemon --console=plain
- store_artifacts:
path: ./android/app/build/outputs/apk/release/app-release.apk
- run-maestro-tests:
platform: android
maestro_file: 'e2e/main.yml'
Commands like install_gems
, install_maestro_cli
, and install_node_modules
are implicit in the full config.yml – mostly boilerplate, so I’ve kept the focus here on the more interesting bits.
5. iOS Job
Setting up the iOS build can be a bit more finicky, but here’s a working approach:
run_maestro_ios:
macos:
xcode: 16.0.0
resource_class: m2pro.medium
steps:
- node/install:
node-version: '20.10.0'
- checkout
- install_node_modules
- install_gems
- run:
name: Install Pods
command: |
cd ios
bundle install
bundle exec pod install
- install_maestro_cli
- run:
name: Boot iPhone Simulator
command: |
xcrun simctl boot "iPhone 15"
xcrun simctl list devices booted
- run:
name: Build iOS App for Simulator
command: |
cd ios
xcodebuild clean build \
-workspace MyApp.xcworkspace \
-scheme MyApp-Dev \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath ./build
- store_artifacts:
path: ./ios/build/Build/Products/Release-iphonesimulator/MyApp-Dev.app
- run-maestro-tests:
platform: ios
maestro_file: 'e2e/main.yml'
Debugging Like a Pro (with Caution)
E2E tests can be tricky – they work fine locally, but suddenly fail on CircleCI. This is often due to environment mismatches, network issues, or emulator quirks. Here’s a reliable way to debug these problems in real-time, but be mindful of the security implications.
- SSH into the CI Machine
First, gain access to your CircleCI machine using SSH. This lets you poke around the live environment:
ssh -p <ci_machine_ip_address>
- Start Maestro Studio
Run Maestro Studio to get a live view of what the emulator is doing:
maestro studio
- Expose the Maestro Studio Network with ngrok
Use ngrok to create a secure tunnel to your CI machine so you can access Maestro Studio remotely:
ngrok authtoken <YOUR_NGROK_AUTH_TOKEN>
ngrok http 9999
⚠️ Security Disclaimer:
Be careful with this approach. Exposing your CI machine to the public internet, even through a secure tunnel like ngrok, can introduce security risks. Make sure you’re not leaking sensitive environment variables or allowing unauthorized access. Use ngrok’s IP whitelisting or basic authentication for an extra layer of security if needed.
- Inspect and Debug in Real-Time
Open the ngrok URL and you should be able to interact with the emulator live, inspect flows, and catch any UI issues or environment mismatches that might be causing your tests to fail.
Final Thoughts
With a bit of upfront setup, you can have reliable, automated E2E tests that keep your React Native app rock solid – without shelling out for expensive testing services. If this guide saved you some late-night headaches, drop a star on the Gist or share your setup in the comments.
Note: The current
config.yml
sets up a cron job to run these E2E tests every midnight. This was just my project choice to keep a consistent test cycle, but feel free to adjust or remove this schedule based on your team’s workflow and testing frequency.
Happy testing! 🚀
Top comments (0)