zoukankan      html  css  js  c++  java
  • Cucumber tests on iPhone/iPad (转载)

    I am sure everybody has heard about Cucumber ( https://github.com/aslakhellesoy/cucumber) – a tool for Behaviour Driver Development where you describe software behavior in natural language that your customer can understand. Through step definitions these behavior descriptions are executed as automated tests. Cucumber serves as documentation, automated tests and development aid.

    My friend and colleague Christian Hedin gave me tips on iCuke. Cucumber has been widely used for testing web applications, but now it’s also possible to test iOS (iPhone and iPad) apps with help of the iCuke library (https://github.com/unboxed/icuke). iCuke uses AppleScript to drive XCode in order to launch your application into the iOS Simulator. A preloaded library is used to inject a small HTTP server into your application. The HTTP server allows you to see an XML representation of the iOS device screen and to emulate input, such as taps, swipes and pinch gestures.

    You can read more about it here:
    http://www.unboxedconsulting.com/blog/cucumber-iphone-icuke
    http://pragprog.com/magazines/2010-07/bdd-on-iphone-icuke

    Really excited about Cucumber on iPhone we decided to give it a try. After installing iCuke and doing some test runs it was clear that some challenges had to be overcome to make this a truly useful tool for Behaviour Driven Development and automated testing of iOS apps.

    Challenge 1: Screen returns the previous screen's xml

    In our test, we wanted to tap a button, come to another screen and expected to see a text. When the test runs the iPhone is driven to the correct screen, e can see the expected text but the test fails anyway. After little debugging we realized that the method screen.xml returns old xml directly after changing the screen.

    Solution:

    We needed to refresh screen before checking if the expected text is on the new screen. iCuke has a method to refresh screen but it is private and we could not use it. So we just added a new method to the existing ICukeWorld class.

    class ICukeWorld
     def refresh_screen
       refresh
       screen
     end
    end

    Calling this method before checking for the presence of the text solved this problem.

    Later on, I forked iCuke and added this and some other methods.

    git://github.com/DavorC/icuke.git

    Challenge 2: Timing

    Everybody knows that using sleep and delays in code is not so flexible.

    sleep 3 # wait 3 seconds

    We want to check something on the screen and give it 3 seconds to finish its loading.

    Is it enough? Maybe, maybe not. Screen content loading could take 0.1 second or 5 seconds or... - You know what I mean.

    In the first case loading is finished quickly and we unnecessarily spend 3 seconds for doing nothing . If we have a lot of delays in our code then our tests would waste a lot of precious time.

    In the second case the delay is not long enough and the test fails.

    Solution:

    We need to write some help functions to wait for different items which are expected: some text, a button, downloading spinner etc.

    Example with wait for text:

    def wait_for_text(text, timeout = @@timeout)
     puts "#{method_name}(#{text}, #{timeout})" if @@debug
     refresh_screen
     start_time = Time.now
     until(screen.exists?(text)) do
       if Time.now - start_time > timeout
         flunk("#{method_name}: Timed out after #{timeout} seconds")
       end
       sleep 0.1
       refresh_screen
     end
    end

    As you can see the method is waiting for the text to appear and does checking every 0.1 second. As soon as the text is found the test continues. If, after given timeout, the text is still not found, the test fails.

    I prefer to use unit test assertions in my tests (flunk is an assert which always fails). To use assertions with cucumber you need to add assertions to the Cucumber World:

    require 'test/unit/assertions'
    World(Test::Unit::Assertions)

    Challenge 3: Different tappable object on screen can have the same text label.

    Identifying objects on the screen only by text is not enough. By default, the first tappable object is tapped. What if we want to tap the second one?

    Solution:

    I created a set of help functions for:

    returning all objects that satisfy some criteria
    returning a specific object
    waiting for a specific object
    checking if a specific object exists

    Objects are described in xml by: type, label, traits and index.

    Here is an example of getting an array of all elements satisfying given attribute values.

    def get_all_elements_by_type_label_and_traits(type, label, traits)
     puts "#{method_name}(#{type}, #{label}, #{traits})" if @@debug
     refresh_screen
     doc = REXML::Document.new(screen.xml.to_s)
     elements = REXML::XPath.match(doc, "//#{type}[@label=#{label.inspect}][@traits=#{traits.inspect}]")
     elements
    end

    Observe how it is easy to parse xml using ruby's REXML library.

    Of course I could write more generic methods and decrease number of code lines - something like:

    get_all_elements(type, options = {})

    but I like readability so I wrote a set of help functions with more specific naming:

    get_element_by_type(type, index = 0)
    get_element_by_type_and_label(type, label, index = 0)
    get_element_by_type_and_traits(type, traits, index = 0)

    get_element_by_type_label_and_traits(type, label, traits, index = 0)
    get_all_elements_by_type(type)
    get_all_elements_by_type_and_label(type, label)
    get_all_elements_by_type_and_traits(type, traits)
    get_all_elements_by_type_label_and_traits(type, label, traits)
    get_all_static_texts()
    get_all_labels_by_type(type)
    get_all_labels_by_traits(traits)
    element_by_type_exists?(type, index = 0)
    element_by_type_and_label_exists?(type, label, index = 0)
    element_by_type_and_traits_exists?(type, traits, index = 0)
    element_by_type_label_and_traits_exists?(type, label, traits, index = 0)
    text_exists?(text)
    wait_for_element_by_type(type, index = 0, timeout = @@timeout)
    wait_for_element_by_type_and_label(type, label, index = 0, timeout = @@timeout)
    wait_for_element_by_type_and_traits(type, traits, index = 0, timeout = @@timeout)
    wait_for_element_by_type_label_and_traits(type, label, traits, index = 0, timeout = @@timeout)
    wait_for_text(text, timeout = @@timeout)
    get_center_of_the_element(element)
    tap_coordinates(x, y)
    double_tap_coordinates(x, y)
    tap_element(element)
    double_tap_element(element)
    tap_text(text)
    double_tap_text(text)

    In order to use these functions you need to use iCuke from:

    git://github.com/DavorC/icuke.git

    Don't forget to use –recursive flag when you clone it:

    git clone --recursive git://github.com/DavorC/icuke.git

    After building and installing the iCuke gem you need to

    require 'icuke/cucumber_ext'

    instead of:

    require 'icuke/cucumber'

    Example of usage

    In your feature-file:

    Background:
     Given "myApp.xcodeproj" is loaded in the simulator
    
    Scenario Outline: User try to login with different invalid credentials with valid signs
     When I am in "Account" section
      And I paste in username "<user>"
      And I paste in password "<pass>"
      And I tap Login button
     Then I will see alert dialog
    
     Examples:
     | user            | pass      |
     | test@test.com   | test      |
     | 123456          | qwertyui  |
     | ..@..com        | . . . €<> |

    step definitions:

    When /I am in "(.*)" section/ do |section|
     wait_for_text(section)
     tap(section)
     wait_for_element_by_type_and_label("UINavigationItemView", section)
    end
    
    When /I paste in username "(.*)"/ do |user|
     label = "E-mail"
     refresh_screen
     assert(get_all_static_texts().include?(label), "No text field with label #{label} was found")
     write_to_mac_clipboard(user)
     paste_clipboard_to_text_field("UITextFieldLabel", label)
    end
    
    When "I tap Login button" do
     wait_for_text("Login")
     tap("Login")
     wait_for_download_indicator_finish
    end
    
    Then "I will see alert dialog" do
     wait_for_element_by_type("UIAlertView")
    end

    Testing both iPhone and iPad
    If you’re testing a universal app that runs on both iPhone and iPad I recommend writing different scenarios for the platforms. iPad in landscape mode is most likely to reuse most of code you have written for iPhone. Place them in different feature files and tag them with e.g. @iphone respective @ipad tags.

    In your env.rb file:

    $PLATFORM = "iphone"
    
    Before('@ipad') do
     $PLATFORM = "ipad"
    end
    
    Before('@iphone') do
     $PLATFORM = "iphone"
    end

    Use in your feature files:

    Given I have started application

    Implementation:

    Given "I have started the application" do
     # ... some code
     Given "\"myApp\" from \"myApp.xcodeproj\" is loaded in the #{$PLATFORM} simulator"
     # ... more code
    end

    If you wants to run iPad simulator in landscape mode:

    def switch_ipad_to_landscape
     if(get_ipad_orientation == PORTRAIT)
       rotate_simulator_left
     end
    end

    where get_ipad_orientation is some application specific method to decide if the simulator is in portrait or landscape mode.

    Running iCuke tests on Hudson server

    It’s really nice to be able to run your test suite at given intervals, or when you commit to the source code repository. To run your iCuke tests on Hudson (which is a popular continuous integration build server) you must start your iPhone simulator from a terminal window. This is easiest to do by launching an AppleScript from Hudson.

    Below are two scripts, one AppleScript and one shell script, that I used to run my iCuke tests from Hudson.

    - run_cuke.scpt

    -- run_cuke.scpt
    tell application "Finder"
     set my_folder_path to container of (path to me) as text
     set posixPath to POSIX path of file my_folder_path
     set scriptPath to posixPath & "cuke.sh"
    end tell
    tell application "Terminal"
     activate
     do script scriptPath
    end tell
    delay 1200 -- it should be enough to finish all tests
    set logscript to "grep -c FAILED " & posixPath & "cuke.log" & " | cat"
    set cuke_failed to do shell script logscript
    if cuke_failed > 0 then
     error "Cucumber tests failed"
    end if
    try -- do not leave terminals after test run
     do shell script "killall 'Terminal'"
    end try

    - cuke.sh

    #!/bin/bash
    scriptpath=$(cd ${0%/*} && echo $PWD/${0##*/})
    SCRIPTFOLDER=`dirname "$scriptpath"`
    echo Cucumber script is run in: $SCRIPTFOLDER
    cd $SCRIPTFOLDER
    cucumber $SCRIPTFOLDER/features --format=html --out $SCRIPTFOLDER/cuke_results.html > $SCRIPTFOLDER/cuke.log

    Add these two scripts to your project.
    Then, add this Cucumber hook to your env.rb file:

    After do |s|
     if(s.failed?)
       puts "Scenario FAILED: <#{s.name}>"
       puts "More info about failure: SCREEN XML"
       puts screen.xml
     else
       puts "Scenario PASS: <#{s.name}>"
     end
    end

    On Hudson, create a new job and copy the configuration from your project's existing job.
    Configure job:
    add to description:

    <a href="/hudson/job/Helios_Cuke/ws/cuke_results.html">Cucumber Results</a>
    <b> | </b>
    <a href="/hudson/job/Helios_Cuke/ws/cuke.log">Debug.log</a>

    add build step (execute shell):

    osascript ${WORKSPACE}/run_cuke.scpt

    Of course you can change all scripts according your needs.
    At the end you should have your iCuke tests running on Hudson and you will get both a nice test report in HTML and the debug output as a plain text.
    There is a lot of potential for doing automated feature tests for iOS using Cucumber and with iCuke and the additions above you’ll hopefully be well on your way for doing BDD in your next iOS project!

    转自:http://blog.jayway.com/2011/02/11/cucumber-tests-on-iphoneipad/

  • 相关阅读:
    规约先行-(六)并发处理
    MySQL选择合适的方式存储时间
    规约先行-(五)集合处理
    规约先行-(四)OOP 规约
    12.20-LaTex git workflow
    6.25-ROS 软件度量
    6.19-rosdoc_lite and 文档构建工具
    12.27-ros-decision making
    12.3-分级并发有限状态机-SMACH
    12.07-rostest学习
  • 原文地址:https://www.cnblogs.com/simonshi2012/p/2159324.html
Copyright © 2011-2022 走看看