敏捷實踐 (2) - appium支持與無法支持的測試


1. 簡介

在探索App自動化測試工具過程中,主要接觸了 MacacaAppium, 已及稍稍看了點 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_flow.png

測試腳本與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

  1. (void) alert_accept Accept the alert.
  2. (String) alert_accept_text Get the text of the alert's accept button.
  3. (void) alert_click(value) iOS only Tap the alert button identified by value.
  4. (void) alert_dismiss Dismiss the alert.
  5. (String) alert_dismiss_text Get the text of the alert's dismiss button.
  6. (String) alert_text Get the alert message text.

button

  1. (Button) button(index) Find a button by index.
  2. (Button) button(text) Get the first button that includes text.
  3. (Array<String>, Array<Buttons>) buttons(text = nil) Get an array of button texts or button elements if text is provided.
  4. (Array<Button>) buttons(text) Get all buttons that include text.
  5. (Button) first_button Get the first button element.
  6. (Button) last_button Get the last button element.

textfield

  1. (Textfield) textfield(index) Find a textfield by index.
  2. (Array<Textfield>) textfields Get an array of textfield elements.
  3. (Textfield) first_textfield Get the first textfield element.
  4. (Textfield) last_textfield Get the last textfield element.
  5. (Textfield) textfield_exact(text) Get the first textfield that matches text.
  6. (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.

  1. (Text) text(index) Find a text by index.
  2. (Array<Text>) texts Get an array of text elements.
  3. (Text) first_text Get the first text element.
  4. (Text) last_text Get the last text element.
  5. (Text) text_exact(text) Get the first element that matches text.
  6. (Text) text(text) Get the first textfield that includes text.

window

  1. (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的開發也能夠持續集成。
極大的降低了全回歸的成本與工作量。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容