移動端UI自動化測試--Appium和Cucumber的完美結合

大綱

├── 簡介
├── 目的
├── UI自動化測試框架的選擇
├── 環境配置
├── 案例
├── 借助Appium來進行元素定位
└── 源碼地址

1.簡介

在日常開發中,自動化測試往往是開發人員比較頭痛的事,特別是UI的自動化測試更是投入大收益小,很多公司情愿多招一個測試人員,也不愿意自己搭建一套UI自動化測試系統。

前幾年使用TDD模式和XCode自帶的XCTest開發過“Lighten”的早期版本,但后來由于各種原因,測試用例“年久失修”基本已經報廢,現在基本全靠人工測試。在使用TDD模式開發的時候,優點挺多,比如能增強自己的全局思維,跳出牛角尖,從使用者的角度去設計接口,減少了很多冗余代碼。當然缺點也明顯,比如開發人員要把大量時間用在編寫測試用例上,而且隨著版本的迭代更新,測試用例也要跟著更新,大大的增加了開發人員的工作量。

這里不詳細討論單元測試和邏輯測試,主要探討一下UI自動化測試的學習和實踐。
項目源碼
腳本源碼

2.目的

  • 在APP交到測試或產品手里的時候,保證最起碼頁面顯示和跳轉邏輯等功能是正確的;
  • 減少后期的開發迭代過程中,基本功能的自測時間;

3.UI自動化測試框架的選擇

基本要求

  • 支持不同平臺的一套框架,包括安卓、蘋果和前端等;
  • 集成自動化框架,對原有項目的侵入盡量要小,接入成本盡量低;
  • 穩定性要好;
  • 可擴展性好;

市場上有很多自動化的框架,比如:Instrumentation、UIAutomator、Appium、UIAutomation、Calabash-ios等待,那我們應該怎樣去選擇呢?

大廠已經為我們開好路了,我們直接上車即可。

根據市場調查,最終我們選擇的UI自動化測試框架是:Appium + Cucumber 的模式,其基本滿足我先前提的所有要求。

那么什么是Appium呢?

原文是英文的,我這里做下總結。

說白了,Appium就是一個適用于native、hybird、mobile web和desktop apps等開發模式并支持模擬器(iOS、Android)和真機(iOS、Android、Windows、Mac)測試的、開源的跨平臺自動化測試工具。Appium支持iOS、Android、Windows等多個平臺的應用程序自動化測試,而且每個平臺都有一個或多個驅動程序支持,我們可以根據不同的平臺安裝和配置驅動程序,具體的看上面文檔。

Appium的優點

  • 1、所有平臺都使用標準化的APIs,你無需重新編譯和修改你的應用;
  • 2、你可以使用任何你喜歡的與WebDriver兼容的語言(如:Java、Objective-C、JavaScript、PHP、Python、Ruby、C#、Clojure、Perl),結合Selenium WebDriver API和指定語言的客戶端框架編寫測試用例;
  • 3、你可以使用任何測試框架;
  • 4、Appium已經內建moblie web和hybird app支持。在同一個腳本中,你能在原生自動化和webView自動化中無縫切換,因為他們都使用了標準的WebDriver模型,這已經成為web自動化測試的標準;

Appium客戶端下載

Cucumber

按照慣例,這里做下總結:

Cucumber是一個能夠理解用普通語言來描述測試用例,支持行為驅動開發(BDD)的自動化測試工具,使用用Ruby編寫,也支持Java和·Net等多種開發語言。

什么叫做用普通語言來描述測試用例呢,看下具體的案例,我的“引導頁”的測試用例:

@guidepage
Feature: 引導頁
  1.首次安裝應用,判斷是否展示引導頁;
    滑到最后一張,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕;
    點擊“登錄/注冊”按鈕,判斷是否展示登錄界面。
  2.滑動到最后一張引導頁,點擊“進入首頁”按鈕,判斷引導頁是否還存在。

  @guide_01
  Scenario: 首次安裝應用,展示引導頁;滑動到最后一張引導頁,展示“登錄/注冊”和“進入首頁”兩個按鈕
    When 展示引導頁
    Then 滑動到最后一頁
    Then 展示“登錄/注冊”和“進入首頁”兩個按鈕
    When 點擊“登錄/注冊”按鈕
    Then 展示登錄界面

  @guide_02
  Scenario: 點擊最后一張引導頁“進入首頁”按鈕,判斷引導頁是否還存在
    When 滑動到最后一張引導頁,點擊“進入首頁”按鈕
    Then 退出引導頁

也許你現在不明白每一行,每一個關鍵字的含義,沒關系,這個文檔上都有。

當然也支持全中文版的,但是感覺區分沒那么明顯,可以通過cucumber --i18n-languages語句查看支持的語言(前提是已經配置好環境),比如中文的,在終端執行cucumber --i18n-keywords zh-CN:

| feature | "功能"  |
| background  | "背景"  |
| scenario  | "場景", "劇本"  |
| scenario_outline | "場景大綱", "劇本大綱"  |
| examples  | "例子"  |
| given | "* ", "假如", "假設", "假定" |
| when  | "* ", "當" |
| then  | "* ", "那么"  |
| and | "* ", "而且", "并且", "同時" |
| but | "* ", "但是"  |
| given (code)  | "假如", "假設", "假定"  |
| when (code) | "當" |
| then (code) | "那么"  |
| and (code)  | "而且", "并且", "同時"  |
| but (code)  | "但是"  |

4.環境配置

Cucumber

Cucumber的安裝和案例請參考文檔,非常詳細

Appium環境配置

Appium文檔

第三方博客iOS版

我這里使用的Ruby語言編寫,所以你可能需要了解下Ruby的基本語法。

環境弄好了,趕緊搞個案例爽一下。

5.案例

(1)、新建文件夾存放項目(AutoTestDemo)

cd Desktop
mkdir AutoTestDemo

進入 AutoTestDemo 目錄

(2)、初始化cucumber

cucumber --init

執行上面命令,會生成如下目錄結構:

features # 存放feature的目錄
├── step_definitions # 存放steps的目錄
└── support # 環境配置
    └── env.rb

(3)、創建Gemfile文件

創建Gemfile文件

touch Gemfile

打開Gemfile,導入Ruby庫

source 'https://www.rubygems.org'
 
gem 'appium_lib',         '~> 9.7.4'
gem 'rest-client',        '~> 2.0.2'
gem 'rspec',              '~> 3.6.0'
gem 'cucumber',           '~> 2.4.0'
gem 'rspec-expectations', '~> 3.6.0'
gem 'spec',               '~> 5.3.4'
gem 'sauce_whisk',        '~> 0.0.13'
gem 'test-unit',          '~> 2.5.5' # required for bundle exec ruby xunit_android.rb

(4)、安裝ruby依賴庫

# 需要先安裝bundle
gem install bundle

# 安裝ruby依賴
bundle install

(5)、新建apps目錄

apps目錄用于存放,被測試的app包

mkdir apps

運行目標項目,在Products文件夾中找到.app結尾的包,放到apps目錄下,等待測試。

打包app包

(6)、配置運行基本信息

  • 1.進入features/support目錄,新建appium.txt文件
  • 2.編輯appium.txt文件,這里只配置了iOS的模擬器和真正代碼
[caps]
# 模擬器
platformName = "ios"
deviceName = "iPhone X"
platformVersion = "11.2"
app = "./apps/AutoUITestDemo.app"
automationName = "XCUITest"
#noReset="true"

# 真機
# platformName = "ios"
# deviceName = "xxx"
# platformVersion = "10.3.3"
# app = "./apps/AutoUITestDemo.app"
# automationName = "XCUITest"
# udid = "xxxx"
# xcodeOrgId = "QT6N53BFV6"
# xcodeSigningId = "ZHH59G3WE3"
# autoAcceptAlerts = "true"  
# waitForAppScript = "$.delay(5000); $.acceptAlert();" # 處理系統彈窗

[appium_lib]
sauce_username = false
sauce_access_key = false

使用xcrun simctl list devices語句查看系統支持的模擬器版本

查看系統支持的模擬器版本
    1. 打開env.rb文件,配置啟動入口
# This file provides setup and common functionality across all features.  It's
# included first before every test run, and the methods provided here can be
# used in any of the step definitions used in a test.  This is a great place to
# put shared data like the location of your app, the capabilities you want to
# test with, and the setup of selenium.

require 'rspec/expectations'
require 'appium_lib'
require 'cucumber/ast'

# Create a custom World class so we don't pollute `Object` with Appium methods
class AppiumWorld
end

caps = Appium.load_appium_txt file: File.expand_path('../appium.txt', __FILE__), verbose: true
# end
Appium::Driver.new(caps, true)
Appium.promote_appium_methods AppiumWorld

World do
  AppiumWorld.new
end

Before { $driver.start_driver }
After { $driver.driver_quit }

(7)、在features目錄下,新建guide.feature文件,用來描述測試用例

@guidepage
Feature: 引導頁
  1.首次安裝應用,判斷是否展示引導頁;
    滑到最后一張,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕;
    點擊“登錄/注冊”按鈕,判斷是否展示登錄界面。
  2.滑動到最后一張引導頁,點擊“進入首頁”按鈕,判斷引導頁是否還存在。

  @guide_01
  Scenario: 首次安裝應用,展示引導頁;滑動到最后一張引導頁,展示“登錄/注冊”和“進入首頁”兩個按鈕
    When 展示引導頁
    Then 滑動到最后一頁
    Then 展示“登錄/注冊”和“進入首頁”兩個按鈕
    When 點擊“登錄/注冊”按鈕
    Then 展示登錄界面

  @guide_02
  Scenario: 點擊最后一張引導頁“進入首頁”按鈕,判斷引導頁是否還存在
    When 滑動到最后一張引導頁,點擊“進入首頁”按鈕
    Then 退出引導頁

我這里寫了兩個測試場景,分別測試彈出登錄界面和進入首頁。測試用例寫好后,我們就開始編寫腳本代碼了,好激動。

(8)、在step_definitions目錄下,新建guide.rb文件,用來存放腳本代碼

  • 在編寫rb腳本之前,這里有個小技巧,就是先用cucumber語法運行一下項目,當然先保證Appium服務器是啟動狀態。
  • 在終端進入項目下,執行cucumber命令。
啟動服務器
運行項目
  • 然后把終端中提示我們要實現的部分拷貝下來,放到rb文件中即可。

  • 最后我們只要在里面去實現我們的業務邏輯就行啦,具體的實現代碼如下:

# author: BruceLi

=begin
  1.首次安裝應用,判斷是否展示引導頁;
    滑到最后一張,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕;
    點擊“登錄/注冊”按鈕,判斷是否展示登錄界面。
  2.滑動到最后一張引導頁,點擊“進入首頁”按鈕,判斷引導頁是否還存在。
=end


# 滾動引導頁到最后一頁
def swipe_to_last_guide_view
    guideIsExist = exists { id("Guide_Page_View") }
    if guideIsExist
      for i in 0...2
        swipe(direction: "left", element: nil)
        sleep(0.25)
      end
    end
  end
  
  # 跳過引導頁
  def dismiss_guide_page
    guideExist = exists { id("Guide_Page_View") }
    puts guideExist ? "存在引導頁面" : "不存在引導頁面" 
    if guideExist
      swipe_to_last_guide_view
      sleep(1)
      button("Guide_Start_Btn").click
      sleep(0.25)
    end
  end


# @guide_01
#   首次安裝應用,判斷是否展示引導頁; 
#   滑到最后一張,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕; 
#   點擊“登錄/注冊”按鈕,判斷是否展示登錄界面。
When(/^展示引導頁$/) do
    guideIsExist = exists { id("Guide_Page_View") } 
    puts guideIsExist ? "存在引導頁面" : "不存在引導頁面" 
    expect(guideIsExist).to be true 
end

Then(/^滑動到最后一頁$/) do
    swipe_to_last_guide_view
    sleep(1)
end

Then(/^展示“登錄\/注冊”和“進入首頁”兩個按鈕$/) do
    $loginBtnIsExist = exists { id("Guide_Login_Btn") }
    puts $loginBtnIsExist ? "存在“登錄/注冊”按鈕" : "不存在“登錄/注冊”按鈕" 
    expect($loginBtnIsExist).to be true

    startBtnIsExist = exists { id("Guide_Start_Btn") }
    puts startBtnIsExist ? "存在“進入首頁”按鈕" : "不存在“進入首頁”按鈕" 
    expect(startBtnIsExist).to be true
end

When(/^點擊“登錄\/注冊”按鈕$/) do
    if $loginBtnIsExist
        button("Guide_Login_Btn").click
        
    else 
        puts "已登錄"
    end
    sleep(1)
end

Then(/^展示登錄界面$/) do
    if $loginBtnIsExist
        loginViewIsExist = exists { id("login_page") }
        puts loginViewIsExist ? "成功展示“登錄界面" : "展示“登錄界面”失敗" 
        expect(loginViewIsExist).to be true
        sleep(1)
    end
end


# @guide_02 
#   滑動到最后一張引導頁,點擊“進入首頁”按鈕,判斷引導頁是否還存在。
When(/^滑動到最后一張引導頁,點擊“進入首頁”按鈕$/) do
    dismiss_guide_page
end

Then(/^退出引導頁$/) do
    guideIsExist = exists { id("Guide_Page_View") } 
    puts guideIsExist ? "引導頁面退出失敗" : "成功退出“引導頁面" 
    expect(guideIsExist).to be false
    sleep(2)
end
  • 打開終端,運行cucumber --tags @guidepage效果,我這里是按照tags來運行的。
play.png

這里所有用到的id都是需要項目源碼里面去設置accessibilityLabel屬性的

// 例如引導頁和最后一頁的兩個按鈕的id設置為:
guideView.accessibilityLabel = "Guide_Page_View"
guideView.logtinButton.accessibilityLabel = "Guide_Login_Btn"
guideView.startButton.accessibilityLabel  = "Guide_Start_Btn"

// 登錄界面
view.accessibilityLabel = "login_page"

如果某些頁面定位不到可以設置屬性isAccessibilityElement為true

以上手動添加屬性(比較笨),這里有大神已經造好的輪子:給UI控件添加自動化測試的標簽拿走。

(9)、元素定位、常用事件和斷言等

元素定位

# 1、使用button查找按鈕
first_button // 查找第一個button
button(value) // 查找第一個包含value的button,返回[UIAButton|XCUIElementTypeButton]對象
buttons(value) // 查找所有包含value的所有buttons,返回[Array<UIAButton|XCUIElementTypeButton>]對象
 
eg:
button("登錄") // 查找登錄按鈕

# 2、使用textfield查找輸入框
first_textfield // 查找第一個textfield
textfield(value) // 查找第一個包含value的textfield,返回[TextField]
 
eg:
textfield("用戶名") // 查找

# 3、使用accessibility_id查找
id(value) // 返回id等于value的元素
 
eg:
id("登錄") // 返回登錄按鈕
id("登錄頁面") // 返回登錄頁面

# 4、通過find查找
find(value) // 返回包含value的元素
find_elements(:class, 'XCUIElementTypeCell') // 通過類名查找
 
eg:
find("登錄頁面")

# 5、通過xpath查找
xpath(xpath_str)

# web元素定位:
# 測試web頁面首先需要切換driver的上下文
web = driver.available_contexts[1]
driver.set_context(web)

# 定位web頁面的元素
driver.find_elements(:css, ".re-bb") # 通過類選擇器.re-bb定位css的元素

更多元素定位語法

常用事件

// 通過坐標點擊
tap(x: 68, y: 171)
 
// 通過按鈕元素點擊
button("登錄").click

// 滑動手勢
swipe(direction:, element: nil) // direction - Either 'up', 'down', 'left' or 'right'.
 
eg: 上滑手勢
swipe(direction: "up", element: nil)

// wait
wait { find("登錄頁面") } // 等待登錄頁面加載完成
 
// sleep
sleep(2) // 延時2秒

更多點擊事件

斷言

# 1. 相等
expect(actual).to eq(expected)  # passes if actual == expected
expect(actual).to eql(expected) # passes if actual.eql?(expected)
expect(actual).not_to eql(not_expected) # passes if not(actual.eql?(expected))

# 2、比較
expect(actual).to be >  expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be <  expected
expect(actual).to be_within(delta).of(expected)

# 3、類型判斷
expect(actual).to be >  expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be <  expected
expect(actual).to be_within(delta).of(expected)

#  4、Bool值比較
expect(actual).to be_truthy   # passes if actual is truthy (not nil or false)
expect(actual).to be true     # passes if actual == true
expect(actual).to be_falsy    # passes if actual is falsy (nil or false)
expect(actual).to be false    # passes if actual == false
expect(actual).to be_nil      # passes if actual is nil
expect(actual).to_not be_nil  # passes if actual is not nil

# 5、錯誤
expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")

# 6、異常
expect { ... }.to throw_symbol
expect { ... }.to throw_symbol(:symbol)
expect { ... }.to throw_symbol(:symbol, 'value')

更多斷言語法

其它

可通過methods方法,查看元素所有可用的屬性和方法

e.g. :
并且(/^點擊返回$/) do
  puts driver.methods
end

輸出結果為:
[:network_connection_type, :network_connection_type=, :location, :location=, 
:set_location, :touch, :lock, :unlock, :reset, :window_size, :shake, :launch_app, :close_app, :device_locked?, :device_time, :current_context, :open_notifications, 
:toggle_airplane_mode, :current_activity, :current_package, :get_system_bars, :get_display_density, :is_keyboard_shown, :get_network_connection, 
:get_performance_data_types, :available_contexts, :set_context, :app_strings, 
:install_app, :remove_app, :app_installed?, :background_app, :hide_keyboard, 
:press_keycode, :long_press_keycode, :set_immediate_value, :push_file, :pull_file, 
:pull_folder, :get_settings, :update_settings, :touch_actions, :multi_touch, :touch_id, 
:toggle_touch_id_enrollment, :ime_deactivate, :ime_activate, :ime_available_engines, :ime_active_engine, :ime_activated, :find_element, :find_elements, :local_storage, 
:session_storage, :remote_status, :rotate, :rotation=, :orientation, :session_id, 
:save_screenshot, :screenshot_as, :file_detector=, :[], :inspect, :first, :close, :all, 
:action, :quit, :get, :ref, :title, :script, :window_handle, :window_handles, :mouse, 
:keyboard, :browser, :navigate, :switch_to, :manage, :current_url, :page_source, 
:execute_script, :execute_async_script, :capabilities, :methods, :singleton_methods,
 :protected_methods, :private_methods, :public_methods, :to_yaml, 
:to_yaml_properties, :psych_to_yaml, :cucumber_instance_exec, :to_json, 
:instance_of?, :public_send, :instance_variable_get, :instance_variable_set, 
:instance_variable_defined?, :remove_instance_variable, :kind_of?, :instance_variables, :tap, :method, :public_method, :singleton_method, 
:awesome_print, :is_a?, :extend, :define_singleton_method, :awesome_inspect, 
:to_enum, :enum_for, :ai, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :object_id, :display, :send, :gem, :to_s, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, 
:taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :frozen?, :!, :==, :!=, :send, :equal?, :instance_eval, :instance_exec, :id, :should, :should_not]

6.借助Appium來進行元素定位,步驟如下:

Appium客服端點擊搜索按鈕
配置運行的信息
元素定位

7.源碼地址

Swift項目源碼

測試腳本項目源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容