前言
上一篇我們講了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部分。
複製程式碼
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探索3-Calabash進階》