While investigating existing automated tools for mobile TDD, we have run into a well-documented and supported library by Xamarin. It’s called calabash, comes with iOS and Android support and has a wide scope of supported user actions, gestures and expectations.
The only drawback we found significant is lack of native Rspec support (cucumber only for now).
This post provides a simple tutorial for using calabash with Rspec to test your iOS applications.
Overview
Calabash library including сucumber step definitions is aggregated within single gem calabash-cucumber.
Basic tutorial for integrating your project with Calabash testing tool is well described here.
We use mixed approach for initial setup by adding Calabash pod and manually copying main project target in Xcode.
As cucumber is a primary framework coming out of the box, some additional steps are required to use it with Rspec.
Setting up Rspec environment
First of all install rspec gem and initialize your project:
gem install rspec //or use Gemfile rspec --init
This will generate the following file structure:
- ./spec folder for all your test examples.
- ./spec/spec_helper.rb file for all configuration and initial setup stuff.
Using predefined cucumber setup and a bit of source investigation, we came up with the following definitions within spec_helper.rb:
require 'sim_launcher' require 'calabash-cucumber/operations' include Calabash::Cucumber::Core include Calabash::Cucumber::TestsHelpers include Calabash::Cucumber::WaitHelpers include Calabash::Cucumber::KeyboardHelpers RSpec.configure do |config| config.treat_symbols_as_metadata_keys_with_true_values = true config.run_all_when_everything_filtered = true config.filter_run :focus config.order = 'random' config.before do @calabash_launcher = Calabash::Cucumber::Launcher.new unless @calabash_launcher.calabash_no_launch? @calabash_launcher.relaunch @calabash_launcher.calabash_notify(self) end end config.before(:each) do element_exists('view') end config.after do unless @calabash_launcher.calabash_no_stop? calabash_exit @calabash_launcher.stop if @calabash_launcher.active? end end end
A before hook was set up to wait for any view to appear – such trick is required to avoid stupid failures because of initial simulator startup delay.
There are a few additional method definitions required to make this setup work:
def escape_quotes(str) str.gsub("'", "\\\\'") end #### emulate cucumber reports def embed(path, type, label) p "storing report: #{path} (type: #{type}, label:#{label})" end
You can add them to your spec_helper.rb.
Sugarize it
Basic ruby modules included in calabash-cucumber gem have predefined steps for
- querying elements
- performing actions/gestures
- matching expectations
We have added simple wrapper methods to make test examples readable and DRY.
Here are some examples that are used in our test scenarios:
Step definitions
module StepHelper WAIT_TIMEOUT = (ENV['WAIT_TIMEOUT'] || 30).to_f STEP_PAUSE = (ENV['STEP_PAUSE'] || 0.5).to_f def wait_to_see_view(expected_mark) wait_for_elements_exist([marked_view_query_string(expected_mark)], :timeout => WAIT_TIMEOUT) end #Touches def touch_the_button(name) touch("button marked:'#{name}'") sleep(STEP_PAUSE) end def touch_view(expected_mark) touch(marked_view_query_string(expected_mark)) sleep(STEP_PAUSE) end def go_back touch("navigationItemButtonView first") sleep(STEP_PAUSE) end #Waiting def wait_for_seconds(seconds) sleep seconds.to_f end #Inputs def enter_text_into_textfield_by_placeholder(text_to_type, placeholder) touch("textField placeholder:'#{placeholder}'") keyboard_enter_text(text_to_type) sleep(STEP_PAUSE) end def enter_text_into_textfield_by_label(text_to_type, label) touch("textField marked:'#{label}'") keyboard_enter_text(text_to_type) sleep(STEP_PAUSE) end def marked_view_query_string(expected_mark) "view marked:'#{expected_mark}'" end ... end
Custom matchers
RSpec::Matchers.define :show_view do |expected_mark| match do (element_exists( "view marked:'#{expected_mark}'" ) or element_exists( "view text:'#{expected_mark}'")) end failure_message_for_should do "No visible view marked '#{expected_mark}' found" end failure_message_for_should_not do "Found visible view marked '#{expected_mark}'" end end RSpec::Matchers.define :show_error_alert do |expected_error_message| match do error_message = query("view className:'UIAlertView' label className:'UILabel'", :text).first error_message == expected_error_message end failure_message_for_should do "No alert with text '#{expected_error_message}' found" end failure_message_for_should_not do "Found visible alert with text '#{expected_error_message}'" end end ...
Conclusions
Adding Rspec support for your acceptance testing using calabash library is really simple.
Approach described above helped us to build and support application test coverage for wide variety of flows and interactions.
Here is a short example on login flow test scenario:
... shared_examples_for "login error" do it do should show_error_alert(alert_error_message) should_not show_view('Asset list zero case') end end ... describe "login error" do before do navigate_to_login_screen stub_api_request_with_error("POST", "/users/sign_in", error_message); submit_login_form end context "wrong credentials" do let(:error_message) {'login_failed'} let(:alert_error_message) {wrong_email_or_password_error_message} it_should_behave_like "login error" end context "server exception/unavailable" do let(:error_message) {'some_error_message'} let(:alert_error_message) {server_unavailable_error_message} it_should_behave_like "login error" end end ...
Using Rspec instead of cucumber gives you a better opportunity to setup context of test examples.
It’s also very important to use any CI server you prefer as it takes some time for full test coverage to run.
Do not hesitate to add your own step definitions and matchers to keep your test examples readable, DRY and clean.