大綱
├── 簡介 ├── 目的 ├── 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自動化測試的標準;
Cucumber
按照慣例,這裏做下總結:
Cucumber是一個能夠理解用普通語言來描述測試用例,支持行爲驅動開發(BDD)的自動化測試工具,使用用Ruby編寫,也支持Java和·Net等多種開發語言。
什麼叫做用普通語言來描述測試用例呢,看下具體的案例,我的“引導頁”的測試用例:
@guidepageFeature: 引導頁 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
Appium環境配置
我這裏使用的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依賴庫
# 需要先安裝bundlegem 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 = falsesauce_access_key = false
使用xcrun simctl list devices
語句查看系統支持的模擬器版本
查看系統支持的模擬器版本
打開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 methodsclass AppiumWorldendcaps = Appium.load_appium_txt file: File.expand_path('../appium.txt', __FILE__), verbose: true# endAppium::Driver.new(caps, true) Appium.promote_appium_methods AppiumWorld World do AppiumWorld.newendBefore { $driver.start_driver } After { $driver.driver_quit }
(7)、在features目錄下,新建guide.feature文件,用來描述測試用例
@guidepageFeature: 引導頁 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 endThen(/^滑動到最後一頁$/) do swipe_to_last_guide_view sleep(1)endThen(/^展示“登錄\/註冊”和“進入首頁”兩個按鈕$/) 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 trueendWhen(/^點擊“登錄\/註冊”按鈕$/) do if $loginBtnIsExist button("Guide_Login_Btn").click else puts "已登錄" end sleep(1)endThen(/^展示登錄界面$/) do if $loginBtnIsExist loginViewIsExist = exists { id("login_page") } puts loginViewIsExist ? "成功展示“登錄界面" : "展示“登錄界面”失敗" expect(loginViewIsExist).to be true sleep(1) endend# @guide_02 # 滑動到最後一張引導頁,點擊“進入首頁”按鈕,判斷引導頁是否還存在。When(/^滑動到最後一張引導頁,點擊“進入首頁”按鈕$/) do dismiss_guide_pageendThen(/^退出引導頁$/) 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)// waitwait { find("登錄頁面") } // 等待登錄頁面加載完成 // sleepsleep(2) // 延時2秒
斷言
# 1. 相等expect(actual).to eq(expected) # passes if actual == expectedexpect(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 == trueexpect(actual).to be_falsy # passes if actual is falsy (nil or false)expect(actual).to be false # passes if actual == falseexpect(actual).to be_nil # passes if actual is nilexpect(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.methodsend輸出結果爲: [: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客服端點擊搜索按鈕
配置運行的信息
作者:青蘋果園
鏈接:https://www.jianshu.com/p/c3db8e5dc306
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。