(本文轉自InfoQ: http://www.infoq.com/cn/articles/refactoring-test-scripts)
介紹
眾所周知,自動化的測試指令碼很難維護。隨著敏捷方法學在企業軟體項目中的廣泛應用,其核心實踐之一——自動化功能測試已經證明了它的價值,同時卻也對項目提出了挑戰。傳統的“錄製-回播”類型的測試載入器也許能協助測試人員很快地建立一系列的測試指令碼,但這些測試代碼最後卻很難維護。原因就是:應用程式在不斷變化
在編程的世界中,“重構”(在不影響軟體外在行為的前提下,改善軟體內部結構的一種方法)已經成為程式員之間頻繁使用的詞彙。簡而言之,通過重構,程式員讓代碼變得更易於理解、設計也更靈活。經驗豐富的敏捷專案經理會給程式員分配一定的時間來重構代碼,或者把重構作為完成使用者故事的一部分。大部分的整合式開發環境(IDE)已經對多種重構方式提供了內建支援。
開發或者維護自動化的測試指令碼的測試人員就沒有這份愜意了,雖然他們也有使自動化的測試指令碼變得可讀和可維護的要求。軟體發布新版本,會伴隨新特性、bug修複和軟體變更,要想跟蹤與之對應的測試指令碼,這很難(而且,測試指令碼越多,這項工作就越困難)
測試重構
對功能測試的重構目標和流程與代碼重構一樣,但有自己的特點:
- 目標受眾
測試載入器的終端使用者包括測試人員、商務分析師,甚至還有客戶。事實是測試人員、商務分析師和客戶一般都不掌握編程技能,整個範式因此而改變。
- 指令碼文法
代碼重構主要是在編譯型語言(比如Java和C#)上得到支援。函數式測試指令碼,可能是XML、廠商專有指令碼、編譯型語言或者指令碼語言(比如Ruby)。根據測試架構不同,重構的使用形式也不同。
- 功能測試專屬重構
很多通用的代碼重構技巧,比如“重新命名”,可以用在功能測試指令碼裡面,它們特定於測試意圖,比如“Move the scripts to run each test case”。
iTest2 IDE
iTest2 IDE 是一款新的功能測試工具,專為測試人員設計,讓他們能夠很輕鬆地開發和維護自動化的測試指令碼。iTest2完全致力於web測試的自動化,它支援的測試架構是使用RSpec文法的rWebUnit(是廣為流行的Watir的一款開源外掛程式)。
iTest2背後的哲學是:容易、簡單。試用顯示:沒有編程經驗的測試人員在指導下,平均只需要少於10分鐘的時間就能編寫他們第一個自動化測試指令碼。藉助於iTest2,測試人員可以開發、維護和驗證功能需求的測試指令碼;開發人員可以驗證特性可用;商務分析師/客戶通過查看測試回合結果(在真實的瀏覽器下,比如IE或者Firefox)來驗證功能需求。
由iTest2建立的測試指令碼可以從命令列運行,也能整合在持續構建伺服器上。
演練
事實勝於雄辯。下面我們就來看看如何使用iTest2提供的重構工具建立兩個測試案例,使它們變得更易理解和維護。
測試計劃
為了練習,我們給Mecury's NewTour網站開發了一些典型但是簡單的web測試指令碼。
| 網站URL |
http://newtours.demoaut.com |
| 測試資料: |
使用者登入:agileway / agileway |
| 測試案例001: |
一個註冊客戶可以選擇單程航行方式,從紐約前往雪梨。 |
| 測試案例002: |
一個註冊客戶可以選擇往返方式,從紐約前往雪梨。 |
| 自動化測試 |
|
| 測試指令碼架構: |
rWebUnit(開源的Watir擴充) |
| 測試執行方法: |
通過命令列或iTest2 IDE |
| 測試編輯器/工具: |
iTest2 IDE |
建立測試案例0011. 建立項目
首先,我們建立一個iTest2項目,指定網站URL。一個簡單的測試指令碼檔案就會被建立出來,如下所示:
load File.dirname(__FILE__) + '/test_helper.rb'test_suite "TODO" do
include TestHelper
before(:all) do
open_browser "http://newtours.demoaut.com"
end test "your test case name" do # add your test scripts here endend
2. 使用iTest2Recorder錄製測試案例001的測試指令碼
我們使用iTest2Recorder,這是Firefox的一個外掛程式,能錄製使用者在Firefox瀏覽器中的操作,並記錄為可執行檔測試指令碼。
enter_text("userName", "agileway")enter_text("password", "agileway")click_button_with_image("btn_signin.gif")click_radio_option("tripType", "oneway")select_option("fromPort", "New York")select_option("toPort", "Sydney")click_button_with_image("continue.gif")assert_text_present("New York to Sydney")3. 把錄好的測試指令碼貼到一個測試指令碼檔案裡面,運行
# ...
test "[001] one way trip" do enter_text("userName", "agileway") enter_text("password", "agileway") click_button_with_image("btn_signin.gif") click_radio_option("tripType", "oneway")
select_option("fromPort", "New York") select_option("toPort", "Sydney") click_button_with_image("continue.gif") assert_text_present("New York to Sydney")end
現在運行測試案例(按右鍵,然後選擇“Run [001] one way trip ”),它通過了!
使用Page對象進行重構
上面的測試指令碼可以工作,而且rWebUnit文法也非常易讀。有人可能對重構的要求提出質疑,也許還會問“使用Page”是怎麼回事?
首先,以現在的格式來看,測試指令碼並不易於維護。假設我們已經有了數百個自動化的測試指令碼,而新發布的軟體修改了使用者認證方式,使用客戶郵箱作為使用者名稱登入,這意味著我們需要在測試指令碼裡面使用‘email’,而不再是‘userName’。在數百個檔案裡面尋找替換,那可不是個好主意。況且,項目成員也喜歡使用項目裡面的通用詞彙,有一個很美妙的名字來稱呼它們:領域專屬語言(DSL)。在測試指令碼裡面也使用這些詞彙就太美妙了。
使用Page對象能很好地做到這一點。一個我們所說的Page對象代表了一個邏輯上的web頁面,它包含了終端使用者在該頁面上可以執行的操作。舉例來說,在我們例子裡面的首頁就包含了三個操作:“輸入使用者名稱”、“輸入密碼”和“點擊登入按鈕”。“使用Page對象進行重構”是指把操作抽取到特定 Page對象的過程,而iTest2提供了對這樣的重構支援,你可以很容易做到這一點。
1. 抽取到HomePage對象
登入功能是發生在首頁上面,我們把這事交給HomePage。使用者登入是一個很常見的功能,我們用了三行語句(輸入使用者名稱、輸入密碼和點擊登入按鈕)完成這個操作。選中這三行代碼,然後在“Refactoring”菜單下單擊“Extract Page...”(快速鍵是Ctrl+Alt+G)。
圖1. “Refactor”菜單——“Extract Page”
如所示,這樣會彈出一個視窗,讓你輸入Page對象的名字和功能名。這裡,我們分別輸入“HomePage”和“login”。
圖2. “Extract Page”對話方塊
選中的3行代碼就被替換成:
home_page = expect_page HomePagehome_page.login
這將會自動建立一個新檔案“pages/home_page.rb”,其內容如下:
class HomePage < RWebUnit::AbstractWebPage
def initialize(browser)
super(browser, "") # TODO: add identity text (in quotes)
end
def login
enter_text("userName", "agileway")
enter_text("password", "agileway")
click_button_with_image("btn_signin.gif")
end
end
再次運行測試案例,它應該還是可以通過。
注意 :正如Martin Fowler指出,重構的節奏:測試、小的改動、測試、小的改動。正是這種節奏保證了重構的迅速和安全。
2. 抽取SelectFlightPage
登入成功之後,顧客進入了航班選擇頁面。與登入頁面不同,這裡的每個操作很可能被不同的開發人員修改,所以我們把每個操作都抽取為一個函數。把游標移到這一行
click_radio_option("tripType", "oneway")
再次執行“Extract to Page... ”重構命令(Ctrl+Alt+G ),給新的Page對象和函數名輸入“SelectFlightPage ”和“select_trip_oneway ”。
select_flight_page = expect_page SelectFlightPageselect_flight_page.select_trip_oneway
3. 繼續抽取更多的操作到SelectFlightPage對象
繼續把“SelectFlightPage”上的操作重構成函數:“select_from_new_york”、“select_to_sydney”和“click_continue”。
test "[1] one way trip" do home_page = expect_page HomePage home_page.login select_flight_page = expect_page SelectFlightPage select_flight_page.select_trip_oneway select_flight_page.select_from_new_york select_flight_page.select_to_sydney select_flight_page.click_continue assert_text_present("New York to Sydney")end
跟往常一樣,我們再一次運行測試案例。
編寫測試案例002
在重構完測試案例001之後,我們現在有了2個Page對象(“HomePage”和“SelectFlightPage”),因此(通過重用它們)編寫測試案例002會容易很多
1. 使用已有的HomePage
iTest2 IDE內建支援Page對象,輸入“ep”再敲“Tab”製表鍵(稱為“snippets”),就能自動補全為“expect_page”並且彈出所有已知的Page對象以供選擇。
圖 3. 自動補全Page對象
我們就能得到
expect_page HomePage
為了使用HomePage,我們需要持有它的控制代碼(在編程世界中,也被稱為‘變數’)。執行“Introduce Page Variable”重構動作(Ctrl+Alt+V)建立一個新變數。
圖 4. ‘Refactor’菜單 - “Introduce Page Variable”功能表項目
home_page = expect_page HomePage
現在在新行中輸入“home_page.”,會自動提示這個Page對象中定義的函數供你選擇。
圖 5. Page對象函數尋找
2. 添加測試案例2需要的方法
測試案例002跟測試案例001很像,區別只在於旅行類型的選擇和斷言。藉助於Recorder,我們可以定義出新的函數:
click_radio_option("tripType", "roundtrip")
把它重構成SelectFlightPage的一個新功能
select_flight_page.select_trip_round
就變成了
test "[2] round trip" do home_page = expect_page HomePage home_page.login select_flight_page = expect_page SelectFlightPage select_flight_page.select_trip_round select_flight_page.select_from_new_york select_flight_page.select_to_sydney select_flight_page.click_continue assert_text_present("New York to Sydney") assert_text_present("Sydney to New York")end
運行測試案例2的測試指令碼(在測試案例2的任意一行之上單擊右鍵,選擇“Run ...”),測試也通過了!
把應用複原為原始狀態
但是等一等,我們還沒有完成。測試案例1通過了,測試案例2也通過了,但是當把它們一起啟動並執行時候,測試案例2卻失敗了,為什嗎?
我們沒有把web應用複原回初始狀態,在運行完測試案例001之後使用者還是保持登入的狀態。為了讓測試之間互相保持獨立,我們要確保每次運行測試都要以登入開始,以退出結束,有始有終。
test "[001] one way trip" do home_page = expect_page HomePage home_page.login # . . .
click_link("SIGN-OFF") goto_page("/")end
test "[002] round trip" do home_page = expect_page HomePage home_page.login # . . . click_link("SIGN-OFF") goto_page("/")end
重複資料刪除代碼
測試指令碼存在著明顯的重複。RSpec架構允許使用者在每個測試案例運行之前或之後執行某些操作。
選中首部兩行(登入功能),按下“Shift + F7”以執行“Move Code”重構。
圖 6. 重構菜單“Move code”
選擇“2 Move to before(:each)”,把這部分操作移到
before(:each) do home_page = expect_page HomePage home_page.loginend
正如名字所示,這兩步操作會在每個測試案例運行之前執行,所以測試案例002裡面的前面兩行也就沒有存在的必要了。我們還可以執行相似的重構,完成“after(:each)”的相關部分。
after(:each) doclick_link("SIGN-OFF")
goto_page("/")
end
最終版本
以下是測試案例001和002的完整的(經過充分重構的)測試指令碼。
load File.dirname(__FILE__) + '/test_helper.rb' test_suite "Complete Test Script" do include TestHelper before(:all) do open_browser "http://newtours.demoaut.com" end before(:each) do home_page = expect_page HomePage home_page.login end
after(:each) do click_link("SIGN-OFF") goto_page("/") end
test "[001] one way trip" do select_flight_page = expect_page SelectFlightPage select_flight_page.select_trip_oneway select_flight_page.select_from_new_york select_flight_page.select_to_sydney select_flight_page.click_continue assert_text_present("New York to Sydney") end test "[002] round trip" do select_flight_page = expect_page SelectFlightPage select_flight_page.select_trip_round select_flight_page.select_from_new_york select_flight_page.select_to_sydney select_flight_page.click_continue assert_text_present("New York to Sydney") assert_text_present("Sydney to New York") end end
適應變化
我們的世界並不完美。在軟體開發行業,事物頻繁發生變更。幸運的是,以上的工作使得測試指令碼不僅僅更易讀,而且也更容易適應變化。
1. 客戶修改了術語
眾所周知,項目使用同一套語言是一個好的實踐,即使在測試指令碼裡面也是如此。舉例來說,客戶現在更傾向於使用“Return Trip”這個名詞,而不再是“Round Trip”。藉助於重構測試指令碼,這很容易做到。
把游標移到“SelectFlightPage”類(pages/select_flight_page.rb)的“select_trip_round”函數,在“Refactoring”菜單下選擇“Rename ...”項(Shift+F6)
圖 7. “Refactor”菜單-“Rename”
然後輸入新的函數名字“select_return_trip”。
圖 8. “Rename Function”對話方塊
測試指令碼其他引用“select_trip_round”的地方就都更改為
select_flight_page.select_return_trip
2. 應用程式的修改
應用程式(來自程式員)的修改就更普遍了。舉例來說,程式員基於某些原因修改了航班選擇頁面,導致HTML頁面上出發城市的屬性從
<select name="fromPort">
改成
<select name="departurePort">
雖然使用者不會察覺到任何變化,測試指令碼(任何訪問這個頁面的測試案例)現在卻會失敗。如果你直接用錄製的指令檔作為測試指令碼,修改的操作將會非常乏味,而且易於引入錯誤。
定位到“SelectFlightPage”的“select_from_new_york”方法(使用快速鍵Ctrl+T選中“select_flight_page ”,再輸入快速鍵Ctrl+F12選擇“select_from_xx ”),把“fromPort”改成“departurePort”。
def select_from_new_yorkselect_option("departurePort", "New York") # from 'fromPort'end
看上去還不賴!
結論
本文我們介紹了在自動化功能測試中使用Page對象,以使測試指令碼易於理解和維護。通過一個使用iTest2 IDE改善測試指令碼過程的實際例子,我們示範了其提供的豐富的重構功能。
引用文獻
Fowler, Martin, et al. Refactoring: Improving the design of existing code, Reading, Mass.: Addison-Wesley, 1999
----------------------------------------------------------------------------------------------------------------------------------------------------
本文來源: 作者 Zhimin Zhan 譯者 金明