簡介
對 Web 應用程式來講,自動化的整合測試是一個非常重要的部分, 然而由於這些測試案例太依賴具體的 Web 頁面的實現細節,這就給編寫和維護帶來的很大的挑戰。 通常來講有兩種方法可以產生 Web 應用程式測試案例。
手工編寫指令碼:測試人員需要知道 Web 頁面上有哪些表單、輸入框、選擇框、按鈕等,以及這些表單元素的名稱,ID 等屬性,然後才能利用一些工具來編寫測試案例。
通過工具錄製產生:比如 IBM Rational Functional Tester 就提供了錄製使用者在 Web 介面的操作,自動產生測試案例的功能。
方法 1 需要測試人員瞭解太多的 Web 頁面細節,這就使得測試人員不能把精力集中在商務邏輯上,一旦 Web 頁面發生變化,將不得不花費大量精力更新指令碼。方法 2 能夠自動產生測試指令碼,但是這些指令碼的可讀性很差,導致很難維護。同樣如果 Web 頁面發生變化,測試人員也需要重新錄製所有的指令碼。
那麼有沒有辦法克服上述問題,讓工作更加輕鬆一點呢?答案是肯定的!
例如一個線上的電子書店,對於使用者購書的情境,我們可以用下面的指令碼來進行整合測試 :
login 'test@test.com','pass4you' // 登入
list_books // 列出書籍
add_to_shop_cart '誰說大象不能跳舞' // 把《誰說大象不能跳舞》這本書加入到購物車中
讀者可以看到, "login" , "list_books", "add_to_shop_cart" 這些術語已經完全脫離了具體的頁面細節,將不會受到頁面變化的影響, 它們是完全面向業務的,準確的體現了應用的商務邏輯,容易理解、易於維護,並且還能拿來和業務人員進行交流,甚至業務人員自己都能編寫測試指令碼。 有這麼多的優點,那麼如何?它們呢?這正是本文要介紹的重點:利用動態語言 Ruby 來實現“業務驅動”的 Web 應用程式測試。
Ruby 介紹
Ruby,中文意思為紅寶石,但是在電腦領域,它代表一種相當優秀的物件導向的指令碼程式語言。它誕生於 1993 年,近年來隨著 Ruby on Rails 這個“Killer application”在 Web 開發領域迅速躥紅。Ruby 在最初設計時吸收了很多別的語言的精華,例如 perl 語言的文本處理能力,Python 語言的簡單性和可讀性,以及方便的擴充能力和強大的可移植能力,Smalltalk 語言的純物件導向文法思想,這就使它具備了很多其他語言的優點。Ruby 的設計理念是盡量減少編程時不必要的瑣碎工作,讓程式員在完成任務的同時充分的享受編程的樂趣。
Ruby 的特點如下:
物件導向:在 Ruby 中,一切皆是對象,包括其他語言中的基礎資料型別 (Elementary Data Type),比如整數。
例如在 Java 中,對一個數求絕對值用 Math.abs(-20), 但在 Ruby 中一切皆對象,-20 這個數也是對象,所以可以這麼做 -20.abs , 是不是更加形象和直觀?
解釋型指令碼語言:無需編譯,直接執行,開發週期短,調試方便。
動態性:已經定義的類可以在運行時修改。
本文的重點不是介紹 Ruby 語言本身,有興趣的讀者可以參見 參考資源 部分。
案例分析
51book
為了展示如何使用 Ruby 進行業務驅動的測試,同時又不讓讀者陷入到過多細節中,本文假想了一個簡單的線上購書應用 ( 簡稱 51book),這個應用支援如下主要功能:
1.登入 : 使用者必須登入才能購買書籍。
圖 1. 登入
2.瀏覽書籍:包括按標題搜尋書籍。
圖 2. 瀏覽和搜尋書籍
3.把書籍添加到購物車中,參見 圖 2 中的“Add to cart”連結。
4.改變購物車中書籍的數量,並且重新計算。
業務操作
通過上面的介紹,讀者應該對 51book 有了一個簡單的瞭解,接下來我們考慮如何進行業務驅動的測試,首先需要定義面向業務的操作,這樣才能在測試案例中使用它們。 簡單起見,我們定義如下業務操作:
表 1. 業務操作
領域專用語言 (Domain Specific Language)
所謂領域專用語言(domain specific language / DSL),其基本思想是“求專不求全”,不像通用目的語言那樣目標範圍涵蓋一切軟體問題, 而是專門針對某一特定問題的電腦語言。正如它的名稱所宣稱的那樣,這種語言並不是通用的,只是專註於某個特定的“領域”, 例如 SQL 語言就是資料庫的 DSL,使用 SQL 可以完成各種各樣資料的操作,而不用關心底層的具體資料庫實現。由於“領域專用”,你想用 SQL 來開發一個傳統型應用程式是不可能的。
我們在上一節定義的 login , add_to_shop_cart , change_quantity 就是針對 51book 線上書店的 DSL。
Martin Fowler 把 DSL 分為兩大類:外部 DSL 和內部 DSL。對外部 DSL 來講,構建它需要做的是:(1) 定義面向領域的全新的文法。(2) 用某種語言編寫解譯器或編譯器 ,由於這種語言是全新的,我們有很多工作需要做;那麼對於內部 DSL 來說,我們可以選定一種靈活的語言,選取它一個文法的子集,並且利用這種語言的動態特性進行定製,這樣就避免了重新打造一個全新語言的龐大工作量。
Ruby 語言具備非常豐富的文法和異常靈活的動態特徵,非常適合建立動態 DSL。本文就是利用 Ruby 來建立 51book 面向測試的 DSL。
用 Ruby DSL 實現業務操作
原理
由於 Ruby 是一種動態指令碼語言,是解釋執行的,它提供了對一段文本進行 “evaluate”執行的方法。也就是說,我們可以提供一段文本(不必是完整的程式),Ruby 就可以在一個特定的上下文中執行它,當然這段文本需要符合 Ruby 的文法。
比如我們有一個檔案 bookshop.txt,它包含了如下文本 : login "andy", "pass4you" , 那麼怎麼執行它呢?首先需要一個上下文,我們可以定義一個類來表示:
清單 1. BookshopDSLBuilder
class BookshopDSLBuilder def self.execute( dsl) builder=new builder.instance_eval(File.read(dsl), dsl) end def login(user=nil,pwd=nil) print user print pwd end end
上面的代碼非常簡單,需要關注的是靜態方法 execute, 當把 bookshop.txt 作為參數來調用它時,會有什麼情況發生呢 ? 聰明的讀者可能已經猜到了,那就是 user 和 pwd 的值會被列印出來。這段代碼展示了 Ruby 語言的兩個重要特點 :
instance_eval 方法會把一段文本當做代碼來執行。執行的上下文就是對象 BookshopDSLBuilder。 所以當它碰到文本 "login" 時,會自動調用真正的方法 login。
在調用一個方法時,可以不加括弧。這就是為什麼 Ruby 會把文本 login "andy","pass4you" 當做一個方法調用的原因。
這兩個特點就給我們搭了一座“橋”,使得我們可以把那個面向業務測試的文本諸如“login”,“add_to_cart”,“search_book”等轉化為對特定方法的調用了。我們就可以在這些方法中實現某些邏輯。
Watir
我們現在已經能夠把業務測試的指令碼和 Ruby 的對象 / 方法串連起來,可是還需要第二座橋把 Ruby 和 Web 應用程式串連起來,這樣才能使業務測試的指令碼驅動 Web 頁面進行測試。我們希望能有一個軟體或工具可以像人一樣來驅動瀏覽器的操作,例如點選連結,填充表單,點擊按鈕等等。當然它也可以檢查頁面的結果,例如期待的文本是否出現等。
開源工具 Watir 就是這樣一個工具,除了具備上述功能外,它和 Ruby 語言還能進行無縫的整合,並且對瀏覽器尤其是 IE 有超強的控制能力。所以我們選取它作為第二座橋。
下面是一個使用 watir 的簡單例子,它進入 Google 的首頁,在搜尋方塊中鍵入 "bookshop", 然後點擊"搜尋"按鈕。 Watir 充分繼承了 Ruby 語言簡單明了的特點,讀者可以看到使用 Watir 的指令碼是相當直觀,相當容易的。
清單 2. Watir 例子
require "watir"ie = Watir::IE.new ie.goto "http://www.google.com" ie.text_field(:name, "q").set "bookshop"ie.button(:name, "btnG").click
實現 Login
有了上面的兩座“橋”,具體的實現就簡單多了,對於每一個業務操作,我們需要做的是 :
(1) 在一個 Ruby 對象中 (BookshopDSLBuilder) 實現一個同名的方法
(2) 在方法實現中,利用 watir 來操作介面元素。當然前提是我們需要知道介面上有哪些元素。
先來看一看 Login 的實現:
清單 3. Login
class BookshopDSLBuilder include Test::Unit::Assertions #include ruby unit 的 Assertion def self.execute( dsl) builder=new builder.instance_eval(File.read(dsl), dsl) builder end def initialize @login_url = 'http://localhost:3000/bookshop/login' #51Book 的入口 #creat a ie instance @ie= Watir::IE.new # 建立一個 Watir 的執行個體 end def login(user=nil,pwd=nil) @ie.goto @login_url @ie.text_field(:id,"user_name").set(user) # 設定使用者名稱 @ie.text_field(:id,"user_password").set(pwd) # 設定密碼 @ie.button(:type,"submit").click # 點擊提交按鈕 end end
實現 add_to_shop_cart
把書籍添加的購物車中這個操作相對複雜,因為它接收的參數是一個書籍的標題,而在介面上"Add to Cart"卻是一個只包含 book id, 不包含標題的連結,所以無法直接定位。
清單 4. Add to Cart
<table width='100%' class='book'> <tr> <td>title:</td> <td>Agile development</td> # 標題在這裡 </tr> <tr> <td>description:</td> <td>The book of agile development</td> </tr> <tr> <td>price:</td> <td>30.0</td> </tr> <tr> <td colspan="2"> #Add_To_Cart Link 卻在這裡 <a href='/bookshop/add_to_cart/1' >Add to Cart</a> </td> </tr> </table>
這種情況下就可以利用 Watir 對 xpath 強大的支援,先找到標題,在從標題找到連結,最後點選連結即可。
清單 5. 使用 XPath
def add_to_cart(title) table = @ie.table(:xpath, "//table[@class='book']/tbody/tr/td[text()='"+title+"']/../../../") if table[1][2].text == title href = table[4][1].links[1].href @ie.link(:href,href).click end end
對於其他的業務操作,具體的實現方式也是大同小異,這裡不再一一介紹,有興趣的讀者可以參見 附件 中的代碼,最後我們來看一個面向業務的 Web 頁面測試例子:
清單 6. 一個完整的例子
login 'andy','pass4you' add_to_cart 'Agile development' add_to_cart 'Savor Blue' add_to_cart 'Programming Ruby' change_quantity 'Agile development',10 change_quantity 'Savor Blue',10 change_quantity 'Programming Ruby',10 recalculate_cart assert_total_price_is 900 search_book 'Ant cookbook' add_to_cart 'Ant cookbook' assert_total_price_is 910
總結
到目前為止,我們已經通過 Ruby 完整的實現了“業務驅動” 的 Web 應用程式測試,實際上我們通過 Ruby 實現了一個面向業務的抽象層,利用 Watir 把業務操作映射到了對 Html 頁面的操作。這樣當 Html 頁面發生了變化的時候,只需要調整映射,而不需要更改業務層的操作。同時由於它們是完全面向業務的,就使得開發人員或測試人員能把精力集中到商務邏輯的測試上,而不用陷入實現的細節。
掌握了該方法以後,讀者可以應用到自己的程式中,可以使得自己的測試編寫簡單,容易理解,易於維護。將會極大的提供 Web 應用程式的測試效率。