Calabash Android Testing: Setup, Steps, and Device Configuration
Calabash Android tests run against a specially instrumented version of your APK. The instrumentation wraps your app with a small HTTP server (powered by Robotium) that Calabash's Ruby client talks to over a local socket. This means you never modify your production binary — the test build is a separate artifact.
This post covers the Android-specific setup. For the general introduction and project structure, see Getting Started with Calabash.
Instrumenting the APK
Calabash Android needs to inject its test server into your APK before it can run tests. The calabash-android build command handles this:
bundle exec calabash-android build path/to/YourApp.apkThis produces YourApp_instrumented.apk in the same directory. The instrumented APK is what gets installed on the device or emulator during test runs. Your original APK is not modified.
If your app uses a custom signing configuration (release keystore), you need to sign the instrumented APK with the same keystore or tests will fail on devices that enforce signature verification. Calabash prompts you to enter keystore credentials during the build step if it detects a signed APK.
Starting an Emulator
For CI or local development, Android Virtual Devices work without any extra configuration:
# List available AVDs
emulator -list-avds
# Start one in the background
emulator -avd Pixel_6_API_33 -no-window -no-audio &
# Wait for it to be fully booted
adb wait-for-device
adb shell getprop sys.boot_completed
# returns "1" when readyCalabash discovers connected devices through adb devices. If multiple devices are connected, set ADB_DEVICE_ARG to target a specific one:
export ADB_DEVICE_ARG="emulator-5554"
bundle exec calabash-android run YourApp_instrumented.apkRunning Tests
bundle exec calabash-android run path/to/YourApp_instrumented.apkTo run a specific feature file:
bundle exec calabash-android run YourApp_instrumented.apk \
features/login.featureWith tags:
bundle exec calabash-android run YourApp_instrumented.apk \
--tags @smokeCalabash installs the APK, launches the test server, runs your Cucumber scenarios, and uninstalls the app when done.
Built-In Android Steps
Calabash ships a library of predefined steps that cover the most common interactions. These are available when you require 'calabash-android/calabash_steps' in env.rb.
Navigation and tapping:
When I press "Submit"
When I press the "Back" button
When I long press "Delete item"
When I press list item 3Text input:
When I enter "user@example.com" as username
When I clear "search_field"
When I type "hello" into field number 1Assertions:
Then I should see "Welcome back"
Then I should not see "Error"
Then I should see a "checkout_button" button
Then the "email_field" field should contain "user@example.com"Scrolling:
When I scroll down
When I scroll up
When I swipe left
When I swipe rightWaiting:
Then I wait for "Loading..." to disappear
Then I wait for the "home_screen" screen to appearThe marked: selector used in custom steps matches the android:contentDescription attribute or the button text, giving you a platform-neutral way to identify elements.
Writing Custom Steps
When the built-in steps are not specific enough, write your own in features/step_definitions/:
# features/step_definitions/checkout_steps.rb
When('I add the first product to cart') do
# Query returns an array of matching elements
products = query("* id:'product_title'")
raise "No products found" if products.empty?
first_product = products.first
tap("* id:'add_to_cart_button' index:0")
end
Then('the cart badge should show {int} items') do |count|
badge_text = query("* id:'cart_badge'", :text).first
expect(badge_text.to_i).to eq(count)
end
When('I fill the checkout form with valid data') do
tap("* id:'name_input'")
keyboard_enter_text('Jane Doe')
tap("* id:'address_input'")
keyboard_enter_text('123 Main St')
tap("* id:'city_input'")
keyboard_enter_text('Springfield')
hide_soft_keyboard
end
When('I swipe the payment slider to confirm') do
swipe(:right, query: "* id:'payment_slider'")
endQuerying Elements
The query method is the core of all element interaction. It takes a UIQuery string and returns an array of hashes describing matched elements:
# By Android resource ID
query("* id:'submit_button'")
# By class name
query("android.widget.Button")
# By text content
query("* text:'Confirm order'")
# By class and index (zero-based)
query("android.widget.EditText index:0")
# Nested: find a TextView inside a RecyclerView item
query("androidx.recyclerview.widget.RecyclerView child android.widget.TextView")
# Read a property from matched elements
query("* id:'price_label'", :text)
# => ["$29.99"]wait_for_element_exists is the reliable way to assert something is on screen, since UI updates may be asynchronous:
wait_for_element_exists("* id:'confirmation_screen'", timeout: 20)Handling Permissions Dialogs
Android runtime permissions (camera, location, notifications) produce system dialogs outside your app's process. Calabash cannot interact with them using the normal tap method because they are in a different UI context.
Use adb shell to grant permissions before the test starts, or accept them through the UIDevice automation APIs. The simplest approach for test builds is to pre-grant permissions via ADB before running the suite:
PACKAGE="com.example.yourapp"
adb shell pm grant $PACKAGE android.permission.CAMERA
adb shell pm grant $PACKAGE android.permission.ACCESS_FINE_LOCATION
adb shell pm grant $PACKAGE android.permission.POST_NOTIFICATIONSFor dialogs that appear mid-test (e.g. because the feature triggers a permission request), write a helper that taps the system dialog button by text:
def dismiss_permission_dialog(allow: true)
button_text = allow ? 'Allow' : 'Deny'
# System dialogs may take a moment to appear
begin
wait_for_element_exists("* text:'#{button_text}'", timeout: 5)
tap("* text:'#{button_text}'")
rescue Calabash::Android::WaitHelpers::WaitError
# No dialog appeared — that is fine
end
endCall this helper in step definitions that trigger permission-sensitive features:
When('I take a profile photo') do
tap("* id:'camera_button'")
dismiss_permission_dialog(allow: true)
wait_for_element_exists("* id:'camera_preview'", timeout: 10)
endHandling System Alerts (e.g. "App not responding")
ANR dialogs and crash dialogs from Android can block tests. Add a global Before hook to dismiss them:
# features/support/hooks.rb
Before do
# Dismiss any stale system dialogs before each scenario
['Wait', 'OK', 'Close app'].each do |button|
begin
tap("* text:'#{button}'") if element_exists?("* text:'#{button}'")
rescue
# ignore
end
end
endScreenshots on Failure
Attach a screenshot to the Cucumber report when a scenario fails:
After do |scenario|
if scenario.failed?
filename = "screenshot_#{Time.now.to_i}.png"
screenshot(path: "reports/screenshots/#{filename}")
embed("reports/screenshots/#{filename}", 'image/png', 'Screenshot on failure')
end
calabash_exit
endCreate the reports/screenshots/ directory before running tests, or add a Before(:all) hook to create it.
Running on a Physical Device
Connect a device with USB debugging enabled. Verify adb devices shows it as device (not unauthorized):
adb devices
# List of devices attached
# R52R801BXXX deviceSet ADB_DEVICE_ARG if more than one device is connected, then run normally:
export ADB_DEVICE_ARG="R52R801BXXX"
bundle exec calabash-android run YourApp_instrumented.apkCalabash installs the instrumented APK on the physical device and runs the test server there. The Ruby process on your machine communicates with it over TCP, forwarded through ADB.
Environment Variables Reference
| Variable | Purpose |
|---|---|
ADB_DEVICE_ARG |
Target a specific device serial |
SCREENSHOT_PATH |
Directory for screenshots |
CALABASH_FULL_CONSOLE_OUTPUT |
Set to 1 for verbose HTTP logs |
TEST_CLOUD_URL |
Used with old App Center integration (now defunct) |