Calabash探索3-Calabash進階

餓了麼物流技術團隊發表於2018-03-19
2017-3-17 | 暴打小女孩| 測試

前言

上一篇我們講了Calabash的基本用法,有了上一篇的經驗,已經可以寫基本的測試指令碼了,只不過一些特殊情況會寫的不那麼方便,這一篇我們講一些Calabash的進階用法:大概是這幾個方向:

  • 在自定義的Steps中使用Query語句。
  • 自定義Steps支援環境變數擴充套件。
  • Hooks。
  • Calabash原始碼修改與擴充套件。

在自定義的Steps中使用Query語句

上一篇我們簡單介紹了Query的用法和通過Query來幫助我們編寫測試指令碼。 但這個是建立在人工查詢結果上,如果可以讓指令碼自己去查,是不是會更便捷? Calabash提供的預定義Steps中,只有極個別幾種View的Steps,類似ImageView等控制元件暫時還沒有這種待遇,按照一般情況,我們只能選擇通過ID,或者人工Query查詢ImageView的index。那麼如果可以只通過肉眼判斷當前ImageView的index就可以編寫指令碼是不更好呢?

我們來自定義一個Step:

When /^I press imageView number (\d+)$/ do |index|
	$imageView = query("android.support.v7.widget.AppCompatImageView")
	touch($editText[index.to_i-1])
end
複製程式碼

為了保證Steps的自然語義,這裡我們也對index做了減1操作。 你也看到了,在Steps的定義體中,是可以使用Query語句的,不過這樣的寫法稍顯繁瑣了

When /^I press imageView number (\d+)$/ do |index|
	tap_when_element_exists("android.support.v7.widget.AppCompatImageView index:#{index.to_i-1}")
end
複製程式碼

tap_when_element_exists本身也支援一定程度的查詢功能。

你可能會問了,既然這些庫方法已經支援這樣的查詢了,在內部寫Query語句看起來也沒啥用了啊。下面我們把Calabash預定義的Steps原始碼拿來瞅瞅你就知道用處在哪裡了:

Then /^I select "([^\"]*)" from "([^\"]*)"$/ do |item_identifier, spinner_identifier|
  spinner = query("android.widget.Spinner marked:'#{spinner_identifier}'")

  if spinner.empty?
    tap_when_element_exists("android.widget.Spinner * marked:'#{spinner_identifier}'")
  else
    touch(spinner)
  end

  tap_when_element_exists("android.widget.PopupWindow$PopupViewContainer * marked:'#{item_identifier}'")
end
複製程式碼

解釋一下:首先查詢是否有對應描述的下拉選單控制元件,查詢的結果是一個陣列,如果結果是空的,那麼就呼叫點選事件嘗試點選一下,不出意外會報錯找不到這個控制元件。 如果不是空的,那就點選這個查詢結果的第零個元素。 touch方法,如果傳遞的引數是一個陣列,會預設點選第零個元素。 然後點選彈出的PopupWindow中的item。

自定義Steps支援環境變數擴充套件

有幾個場景。

  • 如果測試不同的系統或者不同解析度的手機,測試用例內部可能需要細微改變,為了應對這些改變,複製多套指令碼進行修改顯然不是特別合適。
  • 不同賬號有不同的許可權和資料,同一個測試用例可能需要不同的賬號來測試,為了應對不同賬號複製多套指令碼進行修改顯示不是特別合適。
  • 初期想要讓android和iOS無縫的使用一套指令碼不太現實,兩套指令碼幾乎必然,但如果遇到上面的現象,測試中需要使用一套資料進行支援(賬號),這個時候兩邊都維護一套一模一樣的資料就不太合適了。

以上場景的解決實質是,如何在不修改指令碼的情況下,可以改變其中的引數? 其解決方案便是:引入環境變數。

新建一個新的自定義Step:

Then /^I enter \$([^\$]*) into input field number (\d+)$/ do |text_ev, index|
  text = ENV[text_ev]
  enter_text("android.widget.EditText index:#{index.to_i-1}", text)
end
複製程式碼

有兩處改變,方法名中的引數接收處:\$([^\$]*)和方法體中的引數獲取:text = ENV[text_ev] 這裡基本照貓畫虎就好了。

feature中這樣呼叫:

Then I enter $env_account_1 into input field number 1
複製程式碼

$ 後面的變數名是環境變數名。

在執行calabash-android run **.apk前先設定環境變數

set env_account_1 123456
calabash-android run **.apk
複製程式碼

這樣,在指令碼執行過程中,會將123456當做賬號填寫到輸入框中。

但這只是最基礎的用法,當需要的環境變數非常多時,再使用這樣的方式明顯不太合適,這個時候可以將之放入ruby指令碼中,也便於維護。

新建test_data1.rb

ENV["env_account_1"]="1111111111"
ENV["env_password_1"]="123456"
複製程式碼

新建test_data2.rb

ENV["env_account_1"]="222222222"
ENV["env_password_1"]="123456"
複製程式碼

終端中執行如下命令

Crowdsource-android git:(feature/testing) ✗ irb
irb(main):001:0> require './features/test_data1.rb'
=> true
irb(main):002:0>ENV["env_account_1"]
=> "11111111"
irb(main):003:0> exec('calabash-android run debug.apk ')
Feature: Login feature
...

#指令碼執行完畢,切換另一套環境變數
irb(main):001:0> require './features/test_data1.rb'
=> true
irb(main):002:0>ENV["env_account_2"]
=> "2222222"
irb(main):003:0> exec('calabash-android run debug.apk ')
Feature: Login feature
...

複製程式碼

這樣就可以整套整套的替換環境變數進行測試,ENV["env_account_2"]命令用來檢視環境變數的值。

Hooks

關於Hooks,我們在前面第二章有簡單提過一下。Hooks就是在監聽程式執行的某個階段,並做一些事情。在feature/support資料夾下有三個關於Hooks的檔案: app_installation_hooks.rb , app_life_cycle_hooks.rb , hooks.rb 前兩個檔案分別是對app安裝的Hooks,和app生命週期的Hooks。hooks.rb資料夾是空的,由我們自己來編寫。 這裡暫時還沒有做過什麼特別的實踐,我偷個懶,直接將 立成 @richardcao部落格中的關於這一部分的段落摘了過來,原文在這裡:richardcao.me/2016/10/31/…

我們看一個簡單的app_life_cycle_hooks.rb理解理解:

require 'calabash-android/management/adb'
require 'calabash-android/operations'
Before do |scenario|
  start_test_server_in_background
end
After do |scenario|
  if scenario.failed?
    screenshot_embed
  end
  shutdown_test_server
end

這是預設就生成好的,從字面意思上看,就是app生命週期的hook,這裡可以看到,在主要用到了Before和After關鍵字進行操作,這個顯然很容易就看懂了,於是我自己寫了一個在每個step執行之後都等待2秒的hook,下面的程式碼寫在hooks.rb中(這個檔案預設生成是空的):

require 'calabash-android/calabash_steps'
AfterStep do |scenario|
	sleep 2
end

很容易對吧?關於這部分想多瞭解的可以看cucumber wiki中的Hooks部分。

複製程式碼

cucumber wiki中的Hooks部分

Calabash原始碼修改與擴充套件

前面我們提到了兩種自定義Calabash Steps的方法,分別是自定義Steps和封裝已有的Steps。還有第三種更為深入的定製化方案,那就是修改Calabash原始碼自定義Actions,這裡的Actions指的便是我們之前經常會看到的,類似這樣的程式碼: perform_action('swipe', 'right') perform_action('tap_map_marker_by_title', marker_title, 60000)

其中swipe,tap_map_marker_by_title實際是方法名,他們的核心,以Calabash-android為例,實際也是Android工程 java程式碼實現的。如果你有看過其他關於Calabash-android的部落格,會有介紹修改其原始碼,建立自定義的Actions方法的例項。

基本原理是:Calabash目錄/calabash-android/ruby-gem/test-server/instrumentation-backend 是一個Android工程,在其包/src/sh/calaba/instrumentationbackend/actions中實現了我們用到的ruby庫方法。 在其中建立我們自己的Action類,然後進行編譯,即可建立我們自己的庫方法並使用。

但這樣的部落格都寫於2016年之前,目前最新的Calabash-android專案的原始碼已經刪除了/calabash-android/ruby-gem/test-server目錄下的資料夾,通過翻閱Git記錄得知:Calabash-android在2015.12.12日刪除了本地的test_server目錄,將其移動到了新的專案:calabash-android-server中。如果你將Calabash-android專案切換到2016年之前,還可以找到test_server目錄,檢視裡面的Android工程,修改並重新編譯。但我並不建議你這樣去做,使用這種方式擴充套件意味著要放棄 Calabash一年以上的更新進度。

calabash-android-server同樣是開源專案,雖然暫時我還沒搞明白如何在這個專案中修改原始碼並應用到Calabash-android中,但只是單純研究其原始碼還是非常有價值的!

我挑了其中最簡單的一個類,我們來研究一下:

../actions/gestures/ClickOnScreen.java

package sh.calaba.instrumentationbackend.actions.gestures;


import sh.calaba.instrumentationbackend.InstrumentationBackend;
import sh.calaba.instrumentationbackend.Result;
import sh.calaba.instrumentationbackend.actions.Action;
import android.view.Display;


public class ClickOnScreen implements Action {

    @Override
    public Result execute(String... args) {
        Display display = InstrumentationBackend.solo.getCurrentActivity().getWindowManager().getDefaultDisplay();
        
        float x = Float.parseFloat(args[0]);
        float y = Float.parseFloat(args[1]);
        
        int width = display.getWidth();
        int height = display.getHeight();
        
        InstrumentationBackend.solo.clickOnScreen((x/100)*width, (y/100)*height);
        return Result.successResult();
    }

    @Override
    public String key() {
        return "click_on_screen";
    }

}

複製程式碼

先看下Steps的定義中是怎麼用這個方法的:

Then /^I click on screen (\d+)% from the left and (\d+)% from the top$/ do |x, y|
  perform_action('click_on_screen', x, y)
end
複製程式碼

public String key() 方法便是定義Action方法的名稱。public Result execute(String... args)便是實現。

Result返回值表示該動作是否執行成功。如果失敗,會直接拋到終端中進行顯示錯誤資訊。 這裡我們重點關注這個物件:InstrumentationBackend.solo 獲取介面資訊,以及真正實現點選操作的,都是通過solo物件來實現的。那麼這個solo物件是個啥?

我們摘取InstrumentationBackend類的一段程式碼:

...
import com.jayway.android.robotium.solo.SoloEnhanced;
import sh.calaba.instrumentationbackend.automation.CalabashAutomation;
import sh.calaba.instrumentationbackend.query.ui.UIObject;

import java.util.*;

/*
    Utility class based on the current test-server life cycle.
 */
public class InstrumentationBackend {
    private static final String TAG = "InstrumentationBackend";

    public static List<Intent> intents = new ArrayList<Intent>();
    private static Map<ActivityIntentFilter, IntentHookWithCount> intentHooks =
            new HashMap<ActivityIntentFilter, IntentHookWithCount>();

    private static CalabashAutomation calabashAutomation;

    /* Instrumentation does not belong to this class. Here because of old architecture */
    public static Instrumentation instrumentation;

    public static SoloEnhanced solo;
    public static Actions actions;
    
...
複製程式碼

solo實際是com.jayway.android.robotium.solo.SoloEnhanced類。看到這裡應該明白了,Calabash-android對UI的操作核心其實藉助以Robotium來做的。

Robotium是一款國外的Android自動化測試框架,主要針對Android平臺的應用進行黑盒自動化測試,它提供了模擬各種手勢操作(點選、長按、滑動等)、查詢和斷言機制的API,能夠對各種控制元件進行操作

哦,還要提一點,Calabash不支援跨程式的原有就在於Robotium不支援跨程式,所以讓Calabash支援跨程式的契機就在這裡,修改其原始碼,呼叫uiautomator的API即可,具體可行性和方式還待探索,如果你有興趣,不妨我們一起研究呀

總結

OK,大概就是這樣了,Calabash進階部分會隨時更新,修改Calabash-android-server原始碼進行擴充套件的方法我也會繼續研究下去,成功以後會隨時更新部落格。



《Calabash探索1-Run Calabash》

《Calabash探索2-Calabash用法詳解》

《Calabash探索3-Calabash進階》

《Calabash探索4-Calabash踩坑總結》