1. 簡介
在探索App自動化測試工具過程中,主要接觸了 Macaca 和 Appium, 已及稍稍看了點 Calabash。
其中,Calabash 與 Appium都支持使用 Cucumber 編寫測試用例。由于精力有限,Calabash沒有更進一步研究,有興趣的朋友可以看看,交流一下心得。
Macaca在 敏捷實踐 - 我們是如何自動化App驗收標準(一) 一文中有提到過,是我的前端小伙伴Lorne向我推薦的,看了文檔,也裝上試了試,不得不說Macaca非常優秀,上手非常容易,同時支持 NodeJS 與 Python 與 Java 三大主流語言。就是文檔稍有些少。推薦大家試試。
Appium是個非常優秀的App自動化測試工具,支持的語言比Macaca多一些。讓我選擇Appium而不是Macaca的關鍵因數并非所支持語言的多少,而是Appium支持用Ruby+Cucumber來寫測試用例,簡直就像為我們量身定造。沒錯,我最喜歡用的語言首選Ruby,其次NodeJS/Python,再之Java。
我們的測試用例起步于 appium sample-code 中的ruby部分,強烈推薦大家去翻翻代碼。
2. Appium
Appium的詳細介紹信息可以參考 在線文檔,在這里我并不打算太多重復,而且網上也有不少教程了,只想概要的說明一下它是如何工作的。
2.1 Appium的體系結構
Appium是用NodeJS寫的一個HTTP服務,因此使用它需要先安裝NodeJS。它又是通過WebDriverAgent組件在移動設備中控制被測試應用執行測試指令。
2.2 工作流程圖
注意,這個流程圖并非官方圖,只是我隨手畫的幫助大家理解的示意圖。
測試腳本與Appium與被測試的APP是分別各自獨立的進程,甚至分署在不同的主機與真實設備中。
測試的執行過程為
1). 測試腳本調用appium_lib提供的方法, 如 find, click
2). appium_lib調用轉給Selenium WebDriver的Remote Adapter
3). Selenium WebDriver將請求封裝為JSON,以HTTP Rest協議向Appium發送請求
4). Appium收到HTTP請求后,將請求交給相應的AppiumDriver
5). AppiumDriver通過HTTP向運行在終端設備上的WebDriverAgent進程(后臺app)發送請求
6). 設備上的WebDriverAgent解析請求后,通過XCUITest測試套件與目標測試App交互,并獲取結果
7). WebDriverAgent將結果由HTTPResponse返回給Appium
8). Appium再將結果進行包裝處理后,由HTTPResponse返回給3)的Selenium WebDriver
9). Selenium WebDriver再將結果轉換成Ruby對象返回給測試腳本。
2.3 Appium常用指令
官方已經有了非常完整的文檔,我也就不再重復或做些翻譯工作了。
Ruby語言請移步 https://github.com/appium/ruby_lib/blob/master/docs/docs.md
--
Example use of Appium's mobile gesture.
@driver.find_element()
console.rb
uses some code from simple_test.rb and is released under the same license as Appium. The Accessibility Inspector is helpful for discovering button names and textfield values.
Long click on an ImageView in Android.
last_image = find_elements(:tag_name, :ImageView).last
long_press(element: last_image)
Rotate examples.
driver.rotate :landscape
driver.rotate :portrait
-
status["value"]["build"]["revision"]
Discover the Appium rev running on the server. -
driver.keyboard.send_keys "msg"
Sends keys to currently active element
generic
-
source
Prints a JSON view of the current page. -
page
Prints the content descriptions and text values on the current page. -
page_class
Prints the classes found on the current page. -
(Element) find(value)
Returns the first element that contains value. -
(Element) finds(value)
Returns all elements containing value (iOS only for now). -
(Element) name(name)
Returns the first element containing name. Android name is the content description.
iOS uses accessibility label with a fallback to text. -
(Array<Element>) names(name)
Returns all elements containing name. -
(Element) text(text)
Returns the first element containing text. -
(Array<Element>) texts(text)
Returns all elements containing text. -
current_app
Returns information about the current app. Android only.
--
alert
-
(void) alert_accept
Accept the alert. -
(String) alert_accept_text
Get the text of the alert's accept button. -
(void) alert_click(value)
iOS only Tap the alert button identified by value. -
(void) alert_dismiss
Dismiss the alert. -
(String) alert_dismiss_text
Get the text of the alert's dismiss button. -
(String) alert_text
Get the alert message text.
button
-
(Button) button(index)
Find a button by index. -
(Button) button(text)
Get the first button that includes text. -
(Array<String>, Array<Buttons>) buttons(text = nil)
Get an array of button texts or button elements if text is provided. -
(Array<Button>) buttons(text)
Get all buttons that include text. -
(Button) first_button
Get the first button element. -
(Button) last_button
Get the last button element.
textfield
-
(Textfield) textfield(index)
Find a textfield by index. -
(Array<Textfield>) textfields
Get an array of textfield elements. -
(Textfield) first_textfield
Get the first textfield element. -
(Textfield) last_textfield
Get the last textfield element. -
(Textfield) textfield_exact(text)
Get the first textfield that matches text. -
(Textfield) textfield(text)
Get the first textfield that includes text.
text
The Static Text methods have been prefixed with s_
to avoid conflicting with the generic text methods.
-
(Text) text(index)
Find a text by index. -
(Array<Text>) texts
Get an array of text elements. -
(Text) first_text
Get the first text element. -
(Text) last_text
Get the last text element. -
(Text) text_exact(text)
Get the first element that matches text. -
(Text) text(text)
Get the first textfield that includes text.
window
-
(Object) window_size
Get the window's size.
--
e.name # button, text
e.value # secure, textfield
e.type 'some text' # type text into textfield
e.clear # clear textfield
e.tag_name # calls .type (patch.rb)
e.text
e.size
e.location
e.rel_location
e.click
e.send_keys 'keys to send'
e.set_value 'value to set' # ruby_console specific
e.displayed? # true or false depending if the element is visible
e.selected? # is the tab selected?
e.attribute('checked') # is the checkbox checked?
# alert example without helper methods
alert = $driver.switch_to.alert
alert.text
alert.accept
alert.dismiss
# Secure textfield example.
#
# Find using default value
s = textfield 'Password'
# Enter password
s.send_keys 'hello'
# Check value
s.value == ios_password('hello'.length)
--
Driver
start_driver
will restart the driver.
x
will quit the driver and exit Pry.
execute_script
calls $driver.execute_script
find_element
calls $driver.find_element
find_elements
calls $driver.find_elements
no_wait
will set implicit wait to 0. $driver.manage.timeouts.implicit_wait = 0
set_wait
will set implicit wait to default seconds. $driver.manage.timeouts.implicit_wait = default
set_wait(timeout_seconds)
will set implicit wait to desired timeout. $driver.manage.timeouts.implicit_wait = timeout
.click to tap an element.
.send_keys to type on an element.
Raw UIAutomation
execute_script "au.lookup('button')[0].tap()"
is the same as
execute_script 'UIATarget.localTarget().frontMostApp().buttons()[0].tap()'
See app.js for more au methods.
Note that raw UIAutomation commands are not officially supported.
Advanced au.
In this example we lookup two tags, combine the results, wrap with $, and then return the elements.
s = %(
var t = au.lookup('textfield');
var s = au.lookup('secure');
var r = $(t.concat(s));
au._returnElems(r);
)
execute_script s
XPath(UIAutomation)
See #194 for details.
find_element :xpath, 'button'
find_elements :xpath, 'button'
find_element :xpath, 'button[@name="Sign In"]'
find_elements :xpath, 'button[@name="Sign In"]'
find_element :xpath, 'button[contains(@name, "Sign In")]'
find_elements :xpath, 'button[contains(@name, "Sign")]'
find_element :xpath, 'textfield[@value="Email"]'
find_element :xpath, 'textfield[contains(@value, "Email")]'
find_element :xpath, 'text[contains(@name, "Reset")]'
find_elements :xpath, 'text[contains(@name, "agree")]'
3. Appium支持與無法支持的測試
在 2.2 中,可以看到測試代碼并不能直接訪問被測試的App已及它的控件。因此能測試的也就是XCUITest (iOS)套件所支持的方法與屬性。
支持的測試:
查找元素,獲取name value type text size location enabled? displayed? selected?等屬性,或者發送點擊事件,為文本框輸入文字。
這些基本上都屬于功能性測試,可以用來操作輸入框,點擊按鈕,查找某段文字是否存在。
如
1) 打開登錄頁面
2) 找到 “用戶名” 輸入框,輸入 test@test.com
3) 找到 “密碼” 輸入框, 輸入 123456
4) 找到 “登錄” 按鈕,向它發生點擊事件
5) 登錄成功,檢查用戶是否處于主頁面
傳送門
https://github.com/appium/sample-code/blob/master/sample-code/examples/ruby/simple_test.rb
# GETTING STARTED
# -----------------
# This documentation is intended to show you how to get started with a
# simple Appium & appium_lib test. This example is written without a specific
# testing framework in mind; You can use appium_lib on any framework you like.
#
# INSTALLING RVM
# --------------
# If you don't have rvm installed, run the following terminal command
#
# \curl -L https://get.rvm.io | bash -s stable --ruby
#
# INSTALLING GEMS
# ---------------
# Then, change to the example directory:
# cd appium-location/sample-code/examples/ruby
#
# and install the required gems with bundler by doing:
# bundle install
#
# RUNNING THE TESTS
# -----------------
# To run the tests, make sure appium is running in another terminal
# window, then from the same window you used for the above commands, type
#
# bundle exec ruby simple_test.rb
#
# It will take a while, but once it's done you should get nothing but a line
# telling you "Tests Succeeded"; You'll see the iOS Simulator cranking away
# doing actions while we're running.
require 'rubygems'
require 'appium_lib'
APP_PATH = '../../apps/TestApp/build/release-iphonesimulator/TestApp.app'
desired_caps = {
caps: {
platformName: 'iOS',
versionNumber: '8.1',
deviceName: 'iPhone 6',
app: APP_PATH,
},
appium_lib: {
sauce_username: nil, # don't run on Sauce
sauce_access_key: nil
}
}
# Start the driver
Appium::Driver.new(desired_caps).start_driver
module Calculator
module IOS
# Add all the Appium library methods to Test to make
# calling them look nicer.
Appium.promote_singleton_appium_methods Calculator
# Add two numbers
values = [rand(10), rand(10)]
expected_sum = values.reduce(&:+)
# Find every textfield.
elements = textfields
elements.each_with_index do |element, index|
element.type values[index]
end
# Click the first button
button(1).click
# Get the first static text field, then get its text
actual_sum = first_text.text
raise unless actual_sum == (expected_sum.to_s)
# Alerts are visible
button('show alert').click
find_element :class_name, 'UIAAlert' # Elements can be found by :class_name
# wait for alert to show
wait { text 'this alert is so cool' }
# Or by find
find('Cancel').click
# Waits until alert doesn't exist
wait_true { !exists { tag('UIAAlert') } }
# Alerts can be switched into
wait { button('show alert').click } # Get a button by its text
alert = driver.switch_to.alert # Get the text of the current alert, using
# the Selenium::WebDriver directly
alerting_text = alert.text
raise Exception unless alerting_text.include? 'Cool title'
alert_accept # Accept the current alert
# Window Size is easy to get
sizes = window_size
raise Exception unless sizes.height == 667
raise Exception unless sizes.width == 375
# Quit when you're done!
driver_quit
puts 'Tests Succeeded!'
end
end
提醒,由于sample-code中的TestApp比較老了,上面的測試代碼直接跑,是會失敗的。這個是很隱晦的問題導致,新版模擬器(eg. iPhone 6 (iOS 10.0x)), 在運行這個app是,會自動彈出一個告警信息,說“TestApp使用老版本編譯的,會影響系統系能,建議用新xcode重新編譯”。 就是這個alert框,導致測試腳本找不到相應的控件而失敗。
這個事情一開始也卡了我很久,為什么測試用例會通不過?后來是在用Appium Ruby Console的時候找到的。
解決方式有兩個:
一、重新編譯TestApp;
二、測試代碼在“ # Add two numbers” 前加入 sleep(10) 暫停10秒,讓你有機會點擊 alert 上的 OK 按鈕,關掉對話框。
當時不想多事,選擇了二。
現在想想,應該還有三: 就是把sleep 換成 alert_accept 來關掉對話框。 有興趣的可以自己試試。
無法支持的測試:
首先,測試腳本無法直接訪問被測對象,其次,來之于上面XCUITest的限制,關于控件有無邊框,控件狀態,動畫效果,字體變化,顏色,對齊等視覺效果,我們是無法測試的。
關于這一點,stackoverflow 上也有老外在吐槽:
http://stackoverflow.com/questions/31250941/xcuielement-obtain-image-value
http://www.danielhall.io/exploring-the-new-ui-testing-features-of-xcode-7
如,
a) 文章的標題需要根據不同的狀態,用紅色,黑色,灰色等不同的顏色展示。
b) 當點擊“上傳”按鈕后,頭像Image應當加載新的頭像地址。
這種測試通常就屬于無法用自動化來檢驗的。
尤其是我們的App是用React Native開發的,給我們的測試用例帶來了更多的限制與挑戰。
4. 解決方案
自動化測試更多的是在功能性上,流程性上的測試起作用,基本能覆蓋了80%~90%的功能性測試范圍。
對于自動化不好做的AC,我們的處理方式很簡單,把它們放到人工測試的范圍去,當環境、需求、技術發生變化了,能自動化測試了,再自動化,不要太去糾結。
最后,自動化測試并不能代替一切,也無法覆蓋所有的地方。
它的最大價值在于使得App的開發也能夠持續集成。
極大的降低了全回歸的成本與工作量。