大綱
├── 簡介
├── 目的
├── UI自動化測試框架的選擇
├── 環(huán)境配置
├── 案例
├── 借助Appium來進行元素定位
└── 源碼地址
1.簡介
在日常開發(fā)中神凑,自動化測試往往是開發(fā)人員比較頭痛的事辅愿,特別是UI的自動化測試更是投入大收益小谈山,很多公司情愿多招一個測試人員俄删,也不愿意自己搭建一套UI自動化測試系統(tǒng)。
前幾年使用TDD模式和XCode自帶的XCTest開發(fā)過“Lighten”的早期版本,但后來由于各種原因畴椰,測試用例“年久失修”基本已經(jīng)報廢臊诊,現(xiàn)在基本全靠人工測試。在使用TDD模式開發(fā)的時候斜脂,優(yōu)點挺多抓艳,比如能增強自己的全局思維,跳出牛角尖帚戳,從使用者的角度去設(shè)計接口玷或,減少了很多冗余代碼。當然缺點也明顯片任,比如開發(fā)人員要把大量時間用在編寫測試用例上偏友,而且隨著版本的迭代更新,測試用例也要跟著更新对供,大大的增加了開發(fā)人員的工作量位他。
這里不詳細討論單元測試和邏輯測試,主要探討一下UI自動化測試的學習和實踐产场。
項目源碼
腳本源碼
2.目的
- 在APP交到測試或產(chǎn)品手里的時候鹅髓,保證最起碼頁面顯示和跳轉(zhuǎn)邏輯等功能是正確的;
- 減少后期的開發(fā)迭代過程中京景,基本功能的自測時間窿冯;
3.UI自動化測試框架的選擇
基本要求
- 支持不同平臺的一套框架,包括安卓确徙、蘋果和前端等靡菇;
- 集成自動化框架,對原有項目的侵入盡量要小米愿,接入成本盡量低厦凤;
- 穩(wěn)定性要好;
- 可擴展性好育苟;
市場上有很多自動化的框架较鼓,比如:Instrumentation、UIAutomator违柏、Appium博烂、UIAutomation、Calabash-ios等待漱竖,那我們應(yīng)該怎樣去選擇呢禽篱?
大廠已經(jīng)為我們開好路了,我們直接上車即可馍惹。
根據(jù)市場調(diào)查躺率,最終我們選擇的UI自動化測試框架是:Appium + Cucumber 的模式玛界,其基本滿足我先前提的所有要求。
那么什么是Appium呢悼吱?
原文是英文的慎框,我這里做下總結(jié)。
說白了后添,Appium就是一個適用于native笨枯、hybird、mobile web和desktop apps等開發(fā)模式并支持模擬器(iOS遇西、Android)和真機(iOS馅精、Android、Windows粱檀、Mac)測試的硫嘶、開源的跨平臺自動化測試工具。Appium支持iOS梧税、Android、Windows等多個平臺的應(yīng)用程序自動化測試称近,而且每個平臺都有一個或多個驅(qū)動程序支持第队,我們可以根據(jù)不同的平臺安裝和配置驅(qū)動程序,具體的看上面文檔刨秆。
Appium的優(yōu)點
- 1凳谦、所有平臺都使用標準化的APIs,你無需重新編譯和修改你的應(yīng)用衡未;
- 2尸执、你可以使用任何你喜歡的與WebDriver兼容的語言(如:Java、Objective-C缓醋、JavaScript如失、PHP、Python送粱、Ruby褪贵、C#、Clojure抗俄、Perl)脆丁,結(jié)合Selenium WebDriver API和指定語言的客戶端框架編寫測試用例;
- 3动雹、你可以使用任何測試框架槽卫;
- 4、Appium已經(jīng)內(nèi)建moblie web和hybird app支持胰蝠。在同一個腳本中歼培,你能在原生自動化和webView自動化中無縫切換震蒋,因為他們都使用了標準的WebDriver模型,這已經(jīng)成為web自動化測試的標準丐怯;
Cucumber
按照慣例喷好,這里做下總結(jié):
Cucumber是一個能夠理解用普通語言來描述測試用例,支持行為驅(qū)動開發(fā)(BDD)的自動化測試工具读跷,使用用Ruby編寫梗搅,也支持Java和·Net等多種開發(fā)語言。
什么叫做用普通語言來描述測試用例呢效览,看下具體的案例无切,我的“引導(dǎo)頁”的測試用例:
@guidepage
Feature: 引導(dǎo)頁
1.首次安裝應(yīng)用,判斷是否展示引導(dǎo)頁丐枉;
滑到最后一張哆键,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕;
點擊“登錄/注冊”按鈕瘦锹,判斷是否展示登錄界面籍嘹。
2.滑動到最后一張引導(dǎo)頁,點擊“進入首頁”按鈕弯院,判斷引導(dǎo)頁是否還存在辱士。
@guide_01
Scenario: 首次安裝應(yīng)用,展示引導(dǎo)頁听绳;滑動到最后一張引導(dǎo)頁颂碘,展示“登錄/注冊”和“進入首頁”兩個按鈕
When 展示引導(dǎo)頁
Then 滑動到最后一頁
Then 展示“登錄/注冊”和“進入首頁”兩個按鈕
When 點擊“登錄/注冊”按鈕
Then 展示登錄界面
@guide_02
Scenario: 點擊最后一張引導(dǎo)頁“進入首頁”按鈕,判斷引導(dǎo)頁是否還存在
When 滑動到最后一張引導(dǎo)頁椅挣,點擊“進入首頁”按鈕
Then 退出引導(dǎo)頁
也許你現(xiàn)在不明白每一行头岔,每一個關(guān)鍵字的含義,沒關(guān)系鼠证,這個文檔上都有峡竣。
當然也支持全中文版的,但是感覺區(qū)分沒那么明顯量九,可以通過cucumber --i18n-languages
語句查看支持的語言(前提是已經(jīng)配置好環(huán)境)澎胡,比如中文的,在終端執(zhí)行cucumber --i18n-keywords zh-CN
:
| feature | "功能" |
| background | "背景" |
| scenario | "場景", "劇本" |
| scenario_outline | "場景大綱", "劇本大綱" |
| examples | "例子" |
| given | "* ", "假如", "假設(shè)", "假定" |
| when | "* ", "當" |
| then | "* ", "那么" |
| and | "* ", "而且", "并且", "同時" |
| but | "* ", "但是" |
| given (code) | "假如", "假設(shè)", "假定" |
| when (code) | "當" |
| then (code) | "那么" |
| and (code) | "而且", "并且", "同時" |
| but (code) | "但是" |
4.環(huán)境配置
Cucumber
Appium環(huán)境配置
我這里使用的Ruby語言編寫攻谁,所以你可能需要了解下Ruby的基本語法。
環(huán)境弄好了弯予,趕緊搞個案例爽一下戚宦。
5.案例
(1)、新建文件夾存放項目(AutoTestDemo)
cd Desktop
mkdir AutoTestDemo
進入 AutoTestDemo 目錄
(2)锈嫩、初始化cucumber
cucumber --init
執(zhí)行上面命令受楼,會生成如下目錄結(jié)構(gòu):
features # 存放feature的目錄
├── step_definitions # 存放steps的目錄
└── support # 環(huán)境配置
└── env.rb
(3)垦搬、創(chuàng)建Gemfile文件
創(chuàng)建Gemfile文件
touch Gemfile
打開Gemfile,導(dǎo)入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結(jié)尾的包米绕,放到apps目錄下,等待測試馋艺。
(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();" # 處理系統(tǒng)彈窗
[appium_lib]
sauce_username = false
sauce_access_key = false
使用xcrun simctl list devices
語句查看系統(tǒng)支持的模擬器版本
- 打開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: 引導(dǎo)頁
1.首次安裝應(yīng)用,判斷是否展示引導(dǎo)頁率拒;
滑到最后一張崩泡,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕;
點擊“登錄/注冊”按鈕俏橘,判斷是否展示登錄界面。
2.滑動到最后一張引導(dǎo)頁圈浇,點擊“進入首頁”按鈕寥掐,判斷引導(dǎo)頁是否還存在。
@guide_01
Scenario: 首次安裝應(yīng)用磷蜀,展示引導(dǎo)頁召耘;滑動到最后一張引導(dǎo)頁,展示“登錄/注冊”和“進入首頁”兩個按鈕
When 展示引導(dǎo)頁
Then 滑動到最后一頁
Then 展示“登錄/注冊”和“進入首頁”兩個按鈕
When 點擊“登錄/注冊”按鈕
Then 展示登錄界面
@guide_02
Scenario: 點擊最后一張引導(dǎo)頁“進入首頁”按鈕褐隆,判斷引導(dǎo)頁是否還存在
When 滑動到最后一張引導(dǎo)頁污它,點擊“進入首頁”按鈕
Then 退出引導(dǎo)頁
我這里寫了兩個測試場景,分別測試彈出登錄界面和進入首頁庶弃。測試用例寫好后衫贬,我們就開始編寫腳本代碼了,好激動歇攻。
(8)固惯、在step_definitions目錄下,新建guide.rb文件缴守,用來存放腳本代碼
- 在編寫rb腳本之前葬毫,這里有個小技巧镇辉,就是先用
cucumber
語法運行一下項目,當然先保證Appium服務(wù)器是啟動狀態(tài)贴捡。 - 在終端進入項目下忽肛,執(zhí)行
cucumber
命令。
然后把終端中提示我們要實現(xiàn)的部分拷貝下來烂斋,放到rb文件中即可屹逛。
最后我們只要在里面去實現(xiàn)我們的業(yè)務(wù)邏輯就行啦,具體的實現(xiàn)代碼如下:
# author: BruceLi
=begin
1.首次安裝應(yīng)用源祈,判斷是否展示引導(dǎo)頁煎源;
滑到最后一張,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕香缺;
點擊“登錄/注冊”按鈕手销,判斷是否展示登錄界面。
2.滑動到最后一張引導(dǎo)頁图张,點擊“進入首頁”按鈕锋拖,判斷引導(dǎo)頁是否還存在。
=end
# 滾動引導(dǎo)頁到最后一頁
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
# 跳過引導(dǎo)頁
def dismiss_guide_page
guideExist = exists { id("Guide_Page_View") }
puts guideExist ? "存在引導(dǎo)頁面" : "不存在引導(dǎo)頁面"
if guideExist
swipe_to_last_guide_view
sleep(1)
button("Guide_Start_Btn").click
sleep(0.25)
end
end
# @guide_01
# 首次安裝應(yīng)用祸轮,判斷是否展示引導(dǎo)頁兽埃;
# 滑到最后一張,判斷是否展示“登錄/注冊”和“進入首頁”兩個按鈕适袜;
# 點擊“登錄/注冊”按鈕柄错,判斷是否展示登錄界面。
When(/^展示引導(dǎo)頁$/) do
guideIsExist = exists { id("Guide_Page_View") }
puts guideIsExist ? "存在引導(dǎo)頁面" : "不存在引導(dǎo)頁面"
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
# 滑動到最后一張引導(dǎo)頁苦酱,點擊“進入首頁”按鈕售貌,判斷引導(dǎo)頁是否還存在。
When(/^滑動到最后一張引導(dǎo)頁疫萤,點擊“進入首頁”按鈕$/) do
dismiss_guide_page
end
Then(/^退出引導(dǎo)頁$/) do
guideIsExist = exists { id("Guide_Page_View") }
puts guideIsExist ? "引導(dǎo)頁面退出失敗" : "成功退出“引導(dǎo)頁面"
expect(guideIsExist).to be false
sleep(2)
end
- 打開終端颂跨,運行
cucumber --tags @guidepage
效果,我這里是按照tags來運行的扯饶。
這里所有用到的id都是需要項目源碼里面去設(shè)置accessibilityLabel
屬性的
// 例如引導(dǎo)頁和最后一頁的兩個按鈕的id設(shè)置為:
guideView.accessibilityLabel = "Guide_Page_View"
guideView.logtinButton.accessibilityLabel = "Guide_Login_Btn"
guideView.startButton.accessibilityLabel = "Guide_Start_Btn"
// 登錄界面
view.accessibilityLabel = "login_page"
如果某些頁面定位不到可以設(shè)置屬性isAccessibilityElement
為true
以上手動添加屬性(比較笨)恒削,這里有大神已經(jīng)造好的輪子:給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
輸出結(jié)果為:
[: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]