「小衆」之美 ——Ruby在QA自動化中的應用

前言

關於測試領域的自動化,已有不少的文章作過介紹,「黑科技」也比比皆是,如經過Java字節碼技術實現接口的錄製,Fiddler錄製內容轉Python腳本,App中的插樁調試等,可見角度不一樣,對最佳實踐的理解也不同。這裏想要闡述的是,外賣(上海)QA團隊應用相對「小衆」的Ruby,在資源有限的條件下實現自動化測試的一些實踐與經驗分享。前端

背景

加入外賣上海團隊時,共2名QA同窗,分別負責App與M站的功能測試,自動化測試停留在學習北京側接口測試框架的階段,實效上近乎爲0,能力結構上在代碼這部分是明顯薄弱的。而擺在面前的問題是,迴歸測試的工做量較大,特別是M站渠道衆多(4個渠道),移動端API的接口測試需區分多個版本,自動化測試的開展勢在必行。在這樣的條件下,如何快速且有效地搭建並推廣自動化測試體系?在過去對自動化測試的多種嘗試及實踐的總結後,選擇了Ruby。node

Why Ruby?

簡單點說就是:並不聰明的大腦加上「好逸惡勞」的思想,促使我在這些年的自動化測試實踐中,不斷尋找更合適的解決方案。所謂技術,其本質都是站在別人的肩膀上,肩膀的高度也決定了實現目標的快慢,而Ruby正符合所需的一些特徵:mysql

  1. 效率。自身應該算是「純粹」的測試人員,在「測試開發」這重職業並不普及的年代,一直但願有種語言可讓測試的開發效率超過研發,Ruby作到了;
  2. 人性化的語法,各類糖。相似1.day.ago,簡單的表達不須要解釋;
  3. 強大的元編程能力。基於此,DHH放棄了PHP而使用Ruby開發出了Rails,DSL也所以成爲Ruby開發的框架中很是普通的特性,而這對於不少主流語言都是種奢望;
  4. 對於測試來講足夠充足的社區資源。不涉及科學計算,不涉及服務開發,在沒有這些需求的狀況下,Python和Java再也不是必需。

脫離了開發語言的平臺,但在不關注白盒測試的狀況下並沒有太多不妥。當Ruby用於測試開發,基本「屏蔽」了性能上的劣勢,充分展示了敏捷、易用的特色,也是選擇這一技術路線的主要因素。jquery

接口自動化框架Coral-API

框架思路

接口自動化測試方案衆多,我的認爲它們都有本身的適用的範圍和優缺點。UI類工具雖輕鬆實現無碼Case,但在處理接口變更和全鏈路接口流程上多少會顯得有些繁瑣(尤爲在支持數據驅動需求下),過多的規則、變量設置和編碼也相差無幾;錄製類型的方案,更多仍是適合迴歸,對於較全面的接口測試也須要必定的開發量。基於這些權衡考慮,採用一種編碼儘量少、應用面更廣的接口自動化框架實現方式,把它命名爲Coral-API,主要有如下特色:android

  1. 測試數據處理獨立程序員

    • 預先生成測試所需的最終數據,區分單接口測試數據(單接口數據驅動測試)與鏈路測試數據
    • 經過命令行形式的語句解決了參數的多層嵌套及動態數據生成的問題
    • Excel中維護測試數據,最終轉化爲YML或存入DB,折中解決了JSON形式的數據難維護問題
  2. 學習成本低web

    • 框架提供生成通用結構代碼的功能,使測試人員更關注於業務邏輯處理
    • DSL的書寫風格,即使沒有Ruby的語言基礎,也能夠較快掌握基本的接口測試用例編寫
  3. 擴展性ajax

    • 支持Java平臺的擴展
    • 支持HTTP/RPC接口,可根據開發框架擴展
    • 框架基於Rspec,支持多種驗證方式(Build-In Matcher),及支持自定義Matcher,目前實現了JSON去噪的Diff,各類複合的條件比較

以單個接口測試編寫爲例,下圖描述了具體流程:算法

從圖中能夠看到,安裝了Coral-API的gem後,可經過命令行 「coral g {apiname}」 ,經過模板來生成測試數據XLS及對應的數據處理文件(例如ApiOne.rb文件),修改並執行ApiOne.rb文件,則能夠生成最終的測試數據(YML文件)及測試類和Case文件。若是開發框架支持(有途徑可解析出參數),則能夠經過腳本直接生成整個服務下全部接口的測試代碼,實現自動化Case的同步開發。這種處理過程主要是一併解決了如下幾個問題:sql

  1. 複雜結構的測試數據構造
  2. 動態參數的賦值
  3. 測試數據的維護
  4. 測試數據的加載

假設有如下這樣一個接口請求格式,包含一個orderInfo的子節點,及payInfo的list,還須要解決一些變化值的問題,如各類id和time(暫且稱爲動態字段)。通常框架中會以JSON格式來做爲測試用例的請求格式,在代碼中按變量處理動態字段值。JSON做爲請求數據的保存形式,存在一個很大的問題,就是後期維護,尤爲是Case數量較多的時候。所以,考慮仍以Excel爲數據維護的初始形式(使用上更直觀),經過Sheet的嵌套來處理複雜結構,也便於後期接口參數變更後的Case維護。

userId: E000001
requestId: '1938670097'
orderInfo:
 orderId: '6778043386'
 count: '2'
 name: testgoods
payInfo:
- transactionId: '510455433082284'
 payTime: '2017-04-04 13:03:34'
 payType: BOC
- transactionId: '167338836018587'
 payTime: '2017-04-04 13:03:34'
 payType: Wallet
createTime: '2017-04-04 13:03:34'

複製代碼

測試數據的Excel作以下設計,Main中爲第一層參數結構,預期響應另分一個Sheet,子節點和list節點的內容寫在對應的Sheet中,動態值均置爲空,在接口數據類中處理,orderInfo節點和payInfo節點均另寫在新的Sheet中,用於單接口數據驅動的Case與鏈路迴歸用Case分開,固然這會增長一些Case維護的成本,能夠選擇是否區分。

示例的數據結構,經過如下語句便可實現,若是須要爲後續接口測試提供前置步驟的數據,也能夠同步實現,下例中爲後續接口生成了5條請求數據。針對接口參數變更的狀況,能夠修改Excel和數據處理類文件,執行一遍便可,也提供了批量從新生成全部接口數據的腳本。

class Demo < ApiCaseBase
	
  update self.request,:requestId=>'gen_randcode(10)',:createTime=>'get_datetime'
  add_node self.request,"orderInfo",:orderId=>'gen_randcode(10)'
  add_list self.request,"payInfo",:transactionId=>'gen_randcode(15)',:payTime=>'get_datetime'
  
  sheetData={'ForApiOther'=>5}
  
  generate_data self,sheetData do
    update_force @data,:orderId=>'gen_randcode(10)',:createTime=>'get_datetime'
    add_node_force @data,"orderInfo",:orderId=>'gen_randcode(10)'
    add_list_force @data,"payInfo",:transactionId=>'gen_randcode(15)',:payTime=>'get_datetime'
  end
end
複製代碼

Excel做爲Case的維護形式,缺點是Case較多狀況下頻繁讀取比較影響時間。在這種狀況下,考慮到把數據序列化到YML中,啓動執行時接口測試類自動與測試數據進行綁定。在Case中能夠直接使用形如 DemoTest.request[1]的請求數據,提升了速度,結構上也清晰了很多。

接口測試類文件(HTTP接口調用爲例)生成的模板以下,修改對應的接口信息便可,支持DB驗證(代碼塊p這部分是目前惟一須要寫Ruby代碼的地方,固然這是非必需項)。

require 'apicasebase'
 
class PreviewTest
 
  include ApiTestBase
 
  set_cookie
 
  set_domain "Domain_takeaway"
 
  set_port 80
 
  set_path "/waimai/ajax/wxwallet/Preview"
 
  set_method "get"
 
  set_sql "select * from table"
 
  p = proc do |dbres|
    # do something
    # return a hash
  end
 
  set_p p

end
複製代碼

TestCase文件以下,原則上無需修改,只須要在測試數據的Excel中編寫匹配規則及預期輸出,基本上實現了單個接口無編碼的數據驅動測試。

require 'Preview_validate'
 
RSpec.shared_examples "Preview Example" do |key,requestData,expData|
 
    it 'CaseNo'+ key.to_s + ': '+expData['memo'] do
 
      response = PreviewTest.response_of(key)
      
      expect(response).to eval("#{expData['matcher']} '#{expData['expection']}'")
 
    end
end
 
RSpec.describe "Preview接口測試",:project=>'api_m_auto',:author=>'Neil' do
  PreviewTest.request.each{|key,parameter|include_examples "Preview Example",key,PreviewTest.request[key],PreviewTest.expect[key]}
end
複製代碼

接口流程Case編寫就是各獨立接口的業務邏輯串聯,重點是Case的組織,把一些公用的Steps獨立出shared_examples,在主流程的Case中include這些shared_examples便可,關聯的上下游參數 經過全局變量來傳遞。

RSpec.describe "業務流程測試" ,:project=>'api_m_auto',:author =>'Neil' do
  let(:wm_b_client) { WmBClient.new('自配') }
  
  before(:context) do
    init_step
  end
  
  context "在線支付->商家接單->確認收貨->評價" do
    include_examples "OrderAndPay Example",1
    include_examples "AcceptOrder Example"
    include_examples "CommentStep Example"
  end  
end
複製代碼

經過上面的介紹,能夠看到,Case的編寫大部分能夠經過代碼生成實現(熟悉之後部分接口也能夠根據須要進行操做步驟的取捨,如直接編寫YML)。實踐下來的狀況是,從各方面一無全部,17我的日左右的時間,完成了M站API層接口自動化(業務流程9個,單個接口10個)及點評外賣移動端API的接口自動化(業務流程9個,單個接口20個),實現了外賣業務全鏈路接口迴歸,平均每一個業務流Case步驟9個左右。期間也培養了一名以前未接觸過Ruby的同窗,在完成了初版開發後,兩名初級階段的同窗逐步承擔起了框架的改進工做,實現了更多有效的驗證Matcher,並支持了移動端API多版本的測試。以後的迴歸測試不只時間上縮減了50%以上,也經過接口自動化3次發現了問題,其中一次API不一樣版本致使的Bug充分體現了自動化測試的效率。經過ci_reporter,能夠方便地將Rspec的報告格式轉爲JUnit的XML格式,在Jenkins中作對應的展現。

解決接口多版本測試的例子

移動端API自動化中存在的問題就是,一個接口會存在多個版本並存的狀況,有header中內容不一樣的,或formdata內容不一樣的狀況,在接口迴歸中必須都要照顧到,在Coral-API中咱們採用如下方式進行處理。

在config.yml中定義各版本的header。

Domain_takeaway_header:
	v926: '{"connection":"upgrade","x-forwarded-for":"172.24.121.32, 203.76.219.234","mkunionid":"-113876624192351423","pragma-apptype":"com.dianping.ba.dpscope","mktunneltype":"tcp","pragma-dpid":"-113876624192351423","pragma-token":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","user-agent":"MApi 1.1 (dpscope 9.4.0 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","pragma-device":"598f7d44120d0bf9eb7cf1d9774d3ac43faed266","pragma-os":"MApi 1.1 (dpscope 9.2.6 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","mkscheme":"https","x-forwarded-for-port":"60779","X-CAT-TRACE-MODE":"true","network-type":"wifi","x-real-ip":"203.76.219.234","pragma-newtoken":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","pragma-appid":"351091731","mkoriginhost":"mobile.dianping.com","pragma-unionid":"91d9c0e21aca4170bf97ab897e5151ae0000000000040786871"}' 
    v930: '{"connection":"upgrade","x-forwarded-for":"172.24.121.32, 203.76.219.234","mkunionid":"-113876624192351423","pragma-apptype":"com.dianping.ba.dpscope","mktunneltype":"tcp","pragma-dpid":"-113876624192351423","pragma-token":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","user-agent":"MApi 1.1 (dpscope 9.4.0 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","pragma-device":"598f7d44120d0bf9eb7cf1d9774d3ac43faed266","pragma-os":"MApi 1.1 (dpscope 9.3.0 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","mkscheme":"https","x-forwarded-for-port":"60779","X-CAT-TRACE-MODE":"true","network-type":"wifi","x-real-ip":"203.76.219.234","pragma-newtoken":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","pragma-appid":"351091731","mkoriginhost":"mobile.dianping.com","pragma-unionid":"91d9c0e21aca4170bf97ab897e5151ae0000000000040786871"}'
    ......
複製代碼

在接口測試類被加載時會進行全局變量賦值,同時替換header裏對應節點的token,測試數據YML文件中則作這樣的描述,每條數據的header則較方便地被替換。

---
Main:
  1: &DEFAULT
    headers: '<%= $v926 %>'
    host: mobile.51ping.com
    port: '80'
    path: "/deliveryaddresslist.ta"
    search: "?geotype=2&actuallat=31.217329&actuallng=121.415603&initiallat=31.22167778439444&initiallng=121.42671951083571"
    method: GET
    query: '{"geotype":"2","actuallat":"31.217329","actuallng":"121.415603","initiallat":"31.22167778439444","initiallng":"121.42671951083571"}'
    formData: "{}"
    scheme: 'http:'
  2:
    <<: *DEFAULT
    headers: '<%= $v930 %>'
  3:
    <<: *DEFAULT
    headers: '<%= $v940 %>'
  4:
    <<: *DEFAULT
    headers: '<%= $v950 %>'
  5:
    <<: *DEFAULT
    headers: '<%= $v990 %>'

複製代碼

解決RPC接口測試

HTTP接口的測試框架選擇面仍是比較多的,RPC調用的框架如何測試呢?答案就是JRuby + Java的反射調用,在Pigeon接口中咱們已經試點了這種方式,證實是可行的,針對不一樣的RPC框架實現不一樣的Adapter(Jar文件),Coral-API傳參(JSON格式)給Adapter,Adapter經過解析參數進行反射調用,這樣對於框架來講無需改動,只需對部分文件模板稍做調整,也無需在Ruby中混寫Java代碼,實現了最少的代碼量—2行。

UI自動化框架Coral-APP

框架思想

App的UI自動化,Ruby的簡便性更明顯,尤爲Appium提供了對Ruby良好的支持,各類UI框架的優劣就不在此贅述了。綜合比較了Appium與Calabash後,選擇了前者,測試框架選用了更適合業務流描述的Cucumber,沿用了之前在Web自動化中使用的對象庫概念,將頁面元素存儲在CSV中,包括了Android與iOS的頁面對象描述,知足不一樣系統平臺的測試須要。在針對微信M站的UI自動化方案中,還需解決微信WebView的切換,及多窗口的切換問題,appium_lib都提供了較好的支持,下面介紹下結合了Appium及Cucumber的自動化框架Coral-APP。

框架結構以下圖:

step_definitions目錄下爲步驟實現,public_step.rb定義了一些公共步驟,好比微信測試須要用到的上下文切換,Webview裏的頁面切換功能,也能夠經過support目錄下的global_method.rb裏新增的Kernel中的方法來實現。
support/native目錄下爲app測試的配置文件,support/web目錄下爲h5測試的配置文件。 support/env.rb 爲啓動文件,主要步驟以下:

$caps = Appium.load_appium_txt file: File.expand_path('../app/appium.txt', __FILE__), verbose: true
 
$caps[:caps].store("chromeOptions",{"androidProcess":"com.tencent.mm:tools"})
 
$driver = Appium::Driver.new($caps,true)
 
Elements.generate_all_objects
 
Before{$driver.start_driver}

After{$driver.quit_driver}
複製代碼

support/elements下爲對象庫CSV文件,內容以下圖:

support/elements.rb爲對象庫實現,將CSV中的描述轉換爲Elements模塊中對象的功能,這樣在Page中就能夠直接使用相似「Elements.微信我」 這樣的對象描述了。

......
  
def self.define_ui_object(element)
  case $caps[:caps][:platformName].downcase
    when "android"
      idempotently_define_singleton_method(element["OBJNAME"]){$driver.find_element(:"#{element["ATTRIBUTE"]}","#{element["ANDROID_IDENTITY"]}")}
    else
      idempotently_define_singleton_method(element["OBJNAME"]){$driver.find_element(:"#{element["ATTRIBUTE"]}","#{element["IOS_IDENTITY"]}")}
  end
end
 
......
複製代碼

support/pages爲Page層,實現了每一個頁面下的操做,目前把它實現爲Kernel中的方法,採用中文命名,便於閱讀使用。

module Kernel
  def 點擊我
    Elements.微信我.click
  end
 
  def 點擊收藏按鈕
    Elements.微信收藏.click
  end
 
  def 點擊收藏項
    Elements.微信收藏連接.click
  end
 
  def 點擊收藏中的美團外賣連接
    Elements.微信收藏連接URL.click
  end
end
複製代碼

step裏的步驟咱們能夠這樣寫,封裝好足夠的公共步驟或方法,Case的編寫就是這麼簡單。

When /^進入美團外賣M站首頁$/ do
 
  點擊我
 
  點擊收藏按鈕
 
  點擊收藏項
 
  點擊收藏中的美團外賣連接
 
  等待 5
 
  step "切換到微信Webview"
 
  等待 15
 
  step "切換到美團外賣window"
 
end
複製代碼

最終Feature內容以下:

Feature: 迴歸下單主流程
  打開微信->進入首頁->定位->進入自動化商戶->下單->支付->訂單詳情
  Scenario:
    When 進入美團外賣M站首頁
複製代碼

相對於其餘的UI測試框架,使用接近天然語言的描述,提升了Case可讀性,編寫上也沒有其餘框架那麼複雜。固然UI自動化中仍是有一些小難點的,尤爲是Hybrid應用,Appium目前還存在些對使用影響不大的Bug,在框架試用完成的狀況下,將在微信入口體驗優化項目結束後的進一步使用中去總結與完善。

質量工做的自動化

都知道在美團點評,QA還擔負着質量控制的工做,當功能+自動化+性能+其餘測試工做於一身,並且是1:8的測試開發比下,如何去關注質量的改進?答案只有:工具化、自動化。開發這樣一個小系統,技術方案選擇上考慮主要是效率和學習成本,符合敏捷開發的特色,基於這些因素,應用了被稱爲「Web開發的最佳實踐」的Rails框架。

Rails的設計有些顛覆傳統的編程理念,CRUD的實現上不用說了,一行命令便可,數據庫層的操做,經過migration搞定,在Mail,Job等功能的實現上也很是方便,框架都有對應的模塊,而且提供了大量的組件,Session、Cookie、安全密碼、郵件地址校驗都有對應的gem,感受不像是在寫代碼,更像是在配置項目,不知不覺,一個系統雛形就完成了,整理了下項目中使用到的gem,主要有如下這些。

前端相關:

  1. bootstrap-sass Bootstrap框架
  2. jquery-rails jQuery框架
  3. simple_form 優化的form組件
  4. chartkick 堪稱一行代碼便可的圖表組件
  5. hightchart 圖表組件

後端相關:

  1. validates_email_format_of 郵件地址校驗
  2. has_secure_password 安全密碼組件
  3. mysql2 MySQL鏈接組件
  4. cancancan 權限管理組件
  5. sidekiq 隊列中間件
  6. sidekiq-cron 定時Job組件
  7. rest-client Http And Rest Client For Ruby
  8. will_paginate 分頁組件

從搭建開發環境、寫Demo,本身作產品、開發、測試、搭建生產環境、部署,邊參閱文檔邊實現,總共18我的日左右,實現了平臺基礎功能、線上故障問題的管理及通知、測試報告的管理及通知、Sonar數據的抽取(Job及郵件)、Bug數據的抽取(Job)、自動化測試項目的接入、質量數據的Dashboard各種數據圖表展現等功能,如下爲系統功能的兩個示例:

後臺管理界面

線下缺陷周趨勢

應用Rails,團隊較快進入了能夠經過數據進行質量分析的初級階段,固然還有很長的路要走,在從0到1的這個過程當中,仍是較多地體會到了敏捷開發的特性,也充分感覺到了DRY理念。

總結

以上爲半年左右時間內,外賣上海QA團隊在自動化工做上的一些實踐,總的來講,達到必定預期效果,整理這篇文章分享一些心得。所謂的主流與小衆並不是絕對,主要從幾個方面衡量:

  1. 應用領域。Ruby由於性能問題,始終不太主流,但並不意味着它一無可取,用在測試領域,開發效率、DSL的友好性、語言的粘合性、使用者的學習低成本,都能發揮很大的優點。
  2. 使用羣體。不一樣的使用羣體對於技能掌握的要求也是不一樣的,能達到一樣效果甚至超過預期則就能夠選擇哪怕「小衆」的方案。
  3. 環境背景。其實有不少初創公司選擇Ruby做爲初期的技術棧有必定的道理,而這與咱們當初的情景有類似之處,實際效果也體現了語言的特性。

固然應用「小衆」技術,必然要面對很多挑戰:如何迅速培養能掌握相關技術的同窗,與其餘語言平臺的銜接問題,面對團隊的質疑等。尤爲Ruby屬於易學難精的那種,從腳本語言應用層次上升到動態語言設計層次仍是須要必定的學習曲線的,也就是說對於使用者來講是簡單的,對於設計者的能力要求較高,就像流傳的Ruby程序員的進階過程就是魔法師的養成史。

正由於有特點的技術,才值得去研究和學習,就像它的設計者所說,目的就是爲了讓開發人員以爲編程是件快樂的事情。作了這麼些年的測試,還可以不中止寫代碼的腳步,也是由於幾年前開始接觸Ruby。不論未來是否成爲主流,它仍然是測試領域工具語言的不錯選擇,無論之後會出現什麼樣的技術,選型的標準也不會改變。技術的世界沒有主流與小衆,只有理解正確與否,應用得當與否。

寫在最後

美團外賣上海研發中心長期招聘前端、客戶端、後端、QA及數據、算法相關的工程師,歡迎有興趣的同窗發送簡歷到huangzhuolin02@meituan.com。若是對咱們團隊感興趣,能夠關注咱們的專欄

相關文章
相關標籤/搜索