Calabash iOS Testing: Setup, Steps, and Simulator Configuration

Calabash iOS Testing: Setup, Steps, and Simulator Configuration

Calabash iOS works by embedding a small HTTP server (calabash.framework) into a special test build of your app. When the app launches, it listens on port 37265. Your Ruby test code sends HTTP requests to this server to query the view hierarchy and send touch events. The production build of your app never includes the Calabash framework.

This post covers iOS-specific setup. For the shared project structure and Gherkin basics, see Getting Started with Calabash.

Adding the Calabash Framework to Your Xcode Project

The calabash-ios gem ships a setup script that automates most of this:

cd path/to/YourApp
bundle exec calabash-ios setup

The script:

  1. Downloads calabash.framework into calabash-ios/
  2. Creates a new Xcode target named YourApp-cal (a copy of your main target)
  3. Links calabash.framework into the -cal target
  4. Adds a -cal scheme

After running setup, open Xcode and verify the -cal target exists. Build it to confirm there are no linker errors:

xcodebuild \
  -workspace YourApp.xcworkspace \
  -scheme "YourApp-cal" \
  -sdk iphonesimulator \
  -configuration Debug \
  SYMROOT=build

If your project uses CocoaPods, run pod install before building, and use the workspace:

bundle exec calabash-ios setup
pod install
xcodebuild -workspace YourApp.xcworkspace -scheme "YourApp-cal" ...

Pointing Tests at the App Bundle

Calabash needs to know where your built .app lives. Set APP_BUNDLE_PATH:

export APP_BUNDLE_PATH="build/Build/Products/Debug-iphonesimulator/YourApp-cal.app"

Or set it in features/support/env.rb for portability:

# features/support/env.rb
require 'calabash-cucumber/management/sim_manager'
require 'calabash-cucumber/cucumber'

APP_BUNDLE_PATH = ENV['APP_BUNDLE_PATH'] ||
  File.expand_path('../../../build/Build/Products/Debug-iphonesimulator/YourApp-cal.app', __FILE__)

Selecting a Simulator

By default, Calabash launches the default iOS simulator. To target a specific device and OS:

export DEVICE_TARGET="iPhone 15 Pro"
export SDK_VERSION="17.0"

List available simulators with simctl:

xcrun simctl list devices available

To reset a simulator before running tests (useful in CI to ensure clean state):

export RESET_BETWEEN_SCENARIOS="1"

When set, Calabash resets the simulator's app data between each scenario by calling calabash_exit and relaunching.

Built-In iOS Steps

The calabash-cucumber gem provides predefined steps. Enable them in env.rb:

require 'calabash-cucumber/cucumber'

Tapping and pressing:

When I touch "Submit"
When I touch the "back" button
When I long press "Delete"
When I touch done
When I touch return

Text input:

When I use the native keyboard to enter "user@example.com" into the "email" input field
When I clear the "search" input field

Assertions:

Then I should see "Welcome"
Then I should not see "Error"
Then I should see a "checkout_button" button
Then the "email" input field should contain "user@example.com"

Scrolling:

When I scroll down
When I scroll up
When I swipe left on "photo_carousel"

Waiting:

Then I wait for "Loading" to disappear
Then I wait for the "HomeView" to appear

UIQuery: Querying the View Hierarchy

UIQuery is the selector language Calabash uses to find iOS views. It is similar to CSS in structure but targets UIKit class names and accessibility attributes.

By accessibility label (most portable — matches accessibilityLabel property):

query("* marked:'Submit Order'")

By UIKit class:

query("UIButton")
query("UILabel")
query("UITableViewCell")

By class and index (zero-based):

query("UIButton index:0")   # first button
query("UILabel index:2")    # third label

Nested (parent → child):

query("UITableViewCell descendant UILabel")
query("UIView marked:'product_card' child UILabel")

By text value:

query("UILabel text:'Checkout'")

Reading properties:

# Returns array of text strings from all matching labels
query("UILabel", :text)
# => ["Checkout", "$49.99", "Quantity: 2"]

# Return a specific attribute from the first match
query("UITextField marked:'email_field'", :text).first

Combining with wait_for_element_exists:

wait_for_element_exists("UIView marked:'confirmation_view'", timeout: 15)

Custom Step Definitions

# features/step_definitions/product_steps.rb

When('I add product {string} to the cart') do |product_name|
  wait_for_element_exists("UILabel text:'#{product_name}'", timeout: 10)
  tap("UIButton marked:'add_to_cart' index:0")
end

Then('the cart should contain {int} items') do |count|
  badge = query("UILabel marked:'cart_badge'", :text).first
  expect(badge.to_i).to eq(count)
end

When('I complete the checkout form') do
  tap("UITextField marked:'name_field'")
  keyboard_enter_text('Jane Doe')
  tap("UITextField marked:'address_field'")
  keyboard_enter_text('123 Main St')
  tap_keyboard_action_key  # taps the keyboard's "Next" or "Done" key
end

Then('the order confirmation number should be visible') do
  # Regex match on element text
  elements = query("UILabel", :text).compact
  order_number = elements.find { |t| t.match?(/\A[A-Z0-9]{8,}\z/) }
  expect(order_number).not_to be_nil, "No order confirmation number found on screen"
end

Handling iOS Alerts

iOS system alerts (location, notifications, camera access) can block tests. Calabash provides accept_alert and dismiss_alert helpers, but they only work for app-level alerts (UIAlertController). For system-level permission dialogs, you need to interact with the SpringBoard process, which Calabash does not support directly.

Workaround for simulator testing — set permissions programmatically using simctl before launching the app:

# Grant location permission for the app
xcrun simctl privacy booted grant location com.example.yourapp

# Reset all permissions (useful between test runs)
xcrun simctl privacy booted reset all com.example.yourapp

For app-level alerts, use the built-in helpers:

# In a step or hook
begin
  accept_alert
rescue Calabash::Cucumber::WaitHelpers::WaitError
  # No alert present
end

Or with the Gherkin step:

When I accept the alert
When I dismiss the alert

Device Testing (Physical iPhone/iPad)

Simulator testing is faster, but some features (push notifications, Bluetooth, NFC) only work on real devices.

Requirements for device testing:

  • Code signing: the -cal target must be signed with a valid Development profile
  • The device must be registered in your Apple Developer account
  • calabash.framework must be included in the app bundle (setup script handles this for the -cal target)

Set the device UDID:

export DEVICE_TARGET="00008110-001A2B3C4D5E6789"  # your device UDID

Find the UDID with:

xcrun xctrace list devices
# or
idevice_id -l  # requires libimobiledevice

Build for the device (not simulator SDK):

xcodebuild \
  -workspace YourApp.xcworkspace \
  -scheme "YourApp-cal" \
  -sdk iphoneos \
  -configuration Debug \
  SYMROOT=build

Then run:

export APP_BUNDLE_PATH="build/Build/Products/Debug-iphoneos/YourApp-cal.app"
bundle exec cucumber features/

Resigning for Distribution Testing

If you want to test a release build (e.g. from TestFlight or an enterprise channel), you must resign it with a development certificate to include the Calabash server:

bundle exec calabash-ios resign path/to/YourApp.ipa \
  --profile path/to/YourDevelopmentProfile.mobileprovision

This extracts the IPA, relinks calabash.framework, resigns with the development cert, and repacks the IPA.

Checking Calabash Version in the App

Verify that the Calabash server is correctly embedded before running tests:

bundle exec calabash-ios check path/to/YourApp.app

Output shows the embedded server version. Mismatch between the gem version and embedded server version is a common source of failures — always rebuild the -cal target after updating the gem.

Xcode Version Compatibility

Calabash iOS was regularly updated to handle new Xcode and simulator runtimes, but updates stopped when the project was archived in 2021. Known compatibility limits:

  • Works with Xcode 12 and earlier without patches
  • Xcode 13+ requires community forks or workarounds for simulator communication
  • iOS 15+ simulators may experience instability with the embedded server due to changes in the simulator runtime

If you are maintaining a Calabash suite and need to run on Xcode 14+, the community-maintained fork at calabash-community/calabash-ios has patches for newer Xcode versions, though that repository is also minimally maintained.

Read more

Start now free