Calabash Android Testing: Setup, Steps, and Device Configuration

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.apk

This 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 ready

Calabash 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.apk

Running Tests

bundle exec calabash-android run path/to/YourApp_instrumented.apk

To run a specific feature file:

bundle exec calabash-android run YourApp_instrumented.apk \
  features/login.feature

With tags:

bundle exec calabash-android run YourApp_instrumented.apk \
  --tags @smoke

Calabash 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 3

Text input:

When I enter "user@example.com" as username
When I clear "search_field"
When I type "hello" into field number 1

Assertions:

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 right

Waiting:

Then I wait for "Loading..." to disappear
Then I wait for the "home_screen" screen to appear

The 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'")
end

Querying 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_NOTIFICATIONS

For 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
end

Call 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)
end

Handling 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
end

Screenshots 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
end

Create 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    device

Set 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.apk

Calabash 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)

Read more

Start now free