| 2006 年 6 月 27 日 Java ? 社區在推進(jìn)自動(dòng)單元測試方面已經(jīng)做了一項激動(dòng)人心的工作。越來(lái)越多的開(kāi)放源碼框架支持在構建項目的同時(shí)構建自動(dòng)測試套件。Spring framework、JUnit、TestNG 和其他幾個(gè)框架的一些或全部靈感都來(lái)自自動(dòng)測試的思想。盡管如此,一些非 Java 語(yǔ)言和框架具有更多的測試動(dòng)機、更合適的測試工具和更統一的測試視角。通過(guò)觀(guān)察其他框架的測試方式,可以改進(jìn) Java 語(yǔ)言中的測試方式,甚至使用更合適的語(yǔ)言來(lái)測試 Java 代碼。這篇文章是關(guān)于在 Ruby on Rails 上進(jìn)行測試的兩篇文章中的第一篇,將介紹 Rails 單元測試的方式。 捕獲 bug 我還記得當我第一次得到自動(dòng)測試的 bug 時(shí)的情況。在一次大會(huì )上,當我做完叫做 Bitter Java 的演講之后,Mike Clark(Java 社區的自動(dòng)測試大師,性能調整工具 JUnitPerf 的作者(請參閱 參考資料),現在是 Ruby on Rails 專(zhuān)家)走近我。Mike 告訴我有一種方法可以通過(guò)自動(dòng)測試改進(jìn)我的演講。在那次大會(huì )的剩余時(shí)間里,我跟著(zhù)他四處走,看到了我能看到的盡可能多的他的測試會(huì )議。我開(kāi)始使用他推薦的技術(shù),并對把紅條(代表測試失?。┳兂删G條(代表測試通過(guò))上了癮。自動(dòng)測試改變了我思考軟件開(kāi)發(fā)的方式。 | 關(guān)于本系列 在 跨越邊界 系列中,作者 Bruce Tate 提出了這樣一個(gè)觀(guān)點(diǎn):如今的 Java 程序員可以通過(guò)學(xué)習其他方法和語(yǔ)言得到很好的其他思路。自從 Java 明顯成為所有開(kāi)發(fā)項目的最佳選擇以來(lái)編程前景已經(jīng)改變。其他的框架正影響構建 Java 框架的方式,從其他語(yǔ)言學(xué)到的概念可以影響您的 Java 編程。您編寫(xiě)的 Python(或 Ruby、Smalltalk ... )代碼可以改變您處理 Java 編碼的方式。 本系列為您介紹與 Java 開(kāi)發(fā)根本不同,但也可以直接應用于 Java 開(kāi)發(fā)的編程概念和技術(shù)。在一些例子中,需要對技術(shù)進(jìn)行集成以利用它。在另外一些例子中,您將能夠直接應用這些概念。單獨的工具不及其他語(yǔ)言和框架能夠影響 Java 社區中的開(kāi)發(fā)人員、框架甚至基本方法的思想那么重要。 | | Java 社區絕對有自動(dòng)測試的 bug。坦白地說(shuō),我們別無(wú)選擇。競爭壓力迫使許多公司編寫(xiě)越來(lái)越多的代碼,而測試人員越來(lái)越少,同時(shí)每個(gè)開(kāi)發(fā)人員的又必須有更高的生產(chǎn)率。如果不進(jìn)行自動(dòng)測試,得到測試的內容就會(huì )更少,面對現代應用程序不斷增長(cháng)的復雜性,較少的測試不是一個(gè)可行的選擇方案。 在過(guò)去十年中,我們已經(jīng)看到了對測試工具和技術(shù)的研究。JUnit 和 TestNG 都是支持自動(dòng)單元測試的優(yōu)秀工具,而且由日常的開(kāi)發(fā)人員所驅動(dòng)。Selenium 是改進(jìn)集成和功能測試的工具。一套稱(chēng)作敏捷技術(shù) 的新開(kāi)發(fā)過(guò)程告訴人們要更加重視自動(dòng)測試,不要太多地依賴(lài)正式的設計工具,將它們作為提高質(zhì)量的惟一工具。Java 社區已經(jīng)走了很長(cháng)的路。 (請參閱 參考資料,獲得這里討論的工具與技術(shù)的附加信息。) 其他編程社區也有 bug 工具, 其中一些社區使用的自動(dòng)測試要比 Java 開(kāi)發(fā)人員還有多,他們使用自動(dòng)測試經(jīng)驗有完全不同的原因: - Smalltalk 程序員使用自動(dòng)測試已經(jīng)幾乎有 30 年的時(shí)間了,所以通過(guò)動(dòng)態(tài)類(lèi)型化語(yǔ)言使用的一些技術(shù)更加先進(jìn)。
- 集成框架的開(kāi)發(fā)人員的優(yōu)勢是了解框架元素的結構和組合。有些框架,例如 Ruby on Rails,能夠生成測試用例,而且在默認情況下提供測試特性。
- 具有高級元編程(metaprogramming)能力的語(yǔ)言,例如 Ruby and Lisp,允許使用其他語(yǔ)言不支持的一些測試技巧,例如更容易訪(fǎng)問(wèn) mock 對象。
在這一篇和下一篇文章中,將全面理解在 Ruby on Rails 集成開(kāi)發(fā)框架中的測試方式。第 1 部分側重于測試模型對象,并提供一些從 Rails 獲得啟發(fā)的策略,可以用這些策略使 Java 單元測試更有效。第 2 部分把更多時(shí)間花在功能測試和集成測試上。作為 Java 程序員,您對一些概念可能比較熟悉,特別是在測試的時(shí)候,而其他一些概念可以拓展您的理解。
補漏 在這個(gè)系列的 前一期 中,了解了動(dòng)態(tài)類(lèi)型化會(huì )帶來(lái)某些 bug 種類(lèi),靜態(tài)類(lèi)型化語(yǔ)言將在編譯時(shí)捕捉到這些 bug。清單 1 的 Ruby 代碼片段包含四個(gè)不同的 bug,這四個(gè) bug 在運行時(shí)之前都不會(huì )顯露出來(lái): 清單 1. 帶 bug 的 Ruby 代碼 position = "2" #string, where a number was intended position = positoin + 4 #position is misspelled, evaluates to 0 puts "The position is:" + position.to_string #The method should be to_s | 如果編譯器能夠捕捉 bug,那么這類(lèi) bug 解決起來(lái)是小菜一碟,但是如果依賴(lài)解釋器,那么管理這些 bug 就困難得多。為了處理這些微妙的錯誤,動(dòng)態(tài)語(yǔ)言的用戶(hù)長(cháng)期以來(lái)一直依賴(lài)于自動(dòng)測試。在進(jìn)行測試的時(shí)候,比起其他語(yǔ)言,動(dòng)態(tài)語(yǔ)言及其集成環(huán)境在一般意義和特殊意義上都具有顯著(zhù)的優(yōu)勢: - 語(yǔ)言更簡(jiǎn)潔。測試基本上是腳本編程,許多最好的腳本語(yǔ)言都是動(dòng)態(tài)類(lèi)型化的。
- 集成環(huán)境支持的假設可以讓集成測試更容易,也可能更強大。在 Rails 環(huán)境中將看到一些示例。
- 動(dòng)態(tài)語(yǔ)言允許使用更松散的耦合,使一些測試格式更容易實(shí)現。
在了解動(dòng)態(tài)語(yǔ)言開(kāi)發(fā)人員為什么這么熱衷于測試之后,現在是構建一個(gè)需要一些真正測試的實(shí)際應用程序的時(shí)候了。
構建一個(gè)快速 Rails 應用程序 為了進(jìn)展得快些,我采用了一個(gè)保存山地摩托車(chē)路線(xiàn)數據庫的 Rails 應用程序。我將模型的幾個(gè)測試放在一起。如果想和我一起編寫(xiě)代碼,那么所有需要的工具就是一個(gè)數據庫引擎(我使用的是 MySQL)和 Ruby on Rails 1.1 或更新版本(請參閱 參考資料)。第一步是創(chuàng )建 Rails 項目。在命令提示符下輸入 rails trails 命令,清單 2 顯示了命令和結果: 清單 2. 構建 Rails 應用程序 > rails trails create create app/controllers create app/helpers create app/models create app/views/layouts ...partial results deleted... create test/fixtures create test/functional create test/integration create test/mocks/development create test/mocks/test create test/unit create test/test_helper.rb ...partial results deleted... create config/environment.rb create config/environments/production.rb create config/environments/development.rb create config/environments/test.rb ...partial results deleted... create log/server.log create log/production.log create log/development.log create log/test.log | Rails 除了生成空項目什么都沒(méi)做,但是可以看到它正在為您工作。清單 2 創(chuàng )建的目錄中包含: - 應用程序目錄,包括模型、視圖和控制器的子目錄
- 單元測試、功能測試和集成測試的測試目錄
- 為測試而明確創(chuàng )建的環(huán)境
- 測試用例結果的日志
因為 Rails 是一個(gè)集成環(huán)境,所以它可以假設組織測試框架的最佳方式。Rails 也能生成默認測試用例,后面將會(huì )看到。 現在要通過(guò)遷移創(chuàng )建數據庫表,然后用數據庫表創(chuàng )建新數據庫。請鍵入 cd trails 進(jìn)入 trails 目錄。然后生成一個(gè)模型和遷移(migration),如清單 3 所示: 清單 3. 生成一個(gè)模型和遷移 > script/generate model Trail exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/trail.rb create test/unit/trail_test.rb create test/fixtures/trails.yml create db/migrate create db/migrate/001_create_trails.rb | 注意,如果使用 Windows,就必須在命令前加上 Ruby,這樣命令就變成了 ruby script/generate model Trail。 如清單 3 所示,Rails 環(huán)境不僅創(chuàng )建了模型,還創(chuàng )建了遷移、測試用例和測試 fixture。稍后將看到 fixture 和測試的更多內容。遷移讓 Rails 開(kāi)發(fā)人員可以在整個(gè)開(kāi)發(fā)過(guò)程中處理數據庫表中不可避免的更改(請參閱 跨越邊界:研究活動(dòng)記錄)。請編輯您的遷移(在 001_create_trails.rb 中),以添加需要的列,如清單 4 所示: 清單 4. 添加列 class CreateTrails < ActiveRecord::Migration def self.up create_table :trails do |t| t.column :name, :string t.column :description, :text t.column :difficulty, :string end end def self.down drop_table :trails end end | 您需要創(chuàng )建和配置兩個(gè)數據庫:trails_test 和 trails_development。如果想把這個(gè)代碼投入生產(chǎn),那么還需要創(chuàng )建第三個(gè)數據庫 trails_production,但是現在可以跳過(guò)這一步。請用數據庫管理器創(chuàng )建數據庫。我使用的是 MySQL: 清單 5. 創(chuàng )建開(kāi)發(fā)和測試數據庫 mysql> create database trails_development; Query OK, 1 row affected (0.00 sec) mysql> create database trails_test; Query OK, 1 row affected (0.00 sec) | 然后編輯 config/database.yml 中的配置,以反映數據庫的優(yōu)先選擇。我的配置看起來(lái)像這樣: 清單 6. 將數據庫適配器添加到配置中 development: adapter: mysql database: trails_development username: root password: host: localhost test: adapter: mysql database: trails_test username: root password: host: localhost | 現在可以運行遷移,然后把應用程序剩下的部分搭建(scaffold)在一起: 清單 7. 遷移和搭建 > rake migrate ...results deleted... > script/generate scaffold Trail Trails ...results deleted... create app/views/trails ...results deleted... create app/views/trails/_form.rhtml create app/views/trails/list.rhtml create app/views/trails/show.rhtml create app/views/trails/new.rhtml create app/views/trails/edit.rhtml create app/controllers/trails_controller.rb create test/functional/trails_controller_test.rb ...results deleted... | 再次注意,Rails 已經(jīng)為您創(chuàng )建了測試用例??蚣懿粌H為這個(gè)簡(jiǎn)單的小程序生成了視圖和控制器,而且還生成了有助于測試用戶(hù)界面的功能性測試。
對 Rails 應用程序進(jìn)行單元測試 現在是運行一些測試的時(shí)候了。請看第一個(gè)測試,它已經(jīng)在 test/unit/trail_test.rb 中寫(xiě)好了: 清單 8. 第一個(gè)測試 require File.dirname(__FILE__) + ‘/../test_helper‘ class TrailTest < Test::Unit::TestCase fixtures :trails # Replace this with your real tests. def test_truth assert true end end | 確實(shí),這個(gè)測試用例算不了什么,但您可以從中看出如何構架測試代碼,而且自己的測試用例的模板也已經(jīng)就位。請運行測試,如清單 9 所示(包括結果): 清單 9. 運行第一個(gè)測試 > ruby test/unit/trail_test.rb Loaded suite test/unit/trail_test Started EE Finished in 0.027314 seconds. 1) Error: test_truth(TrailTest): ActiveRecord::StatementInvalid: Mysql::Error: #42S02Table ‘trails_test.trails‘ doesn‘t exist: DELETE FROM trails ...results deleted... | 測試用例失敗,但是請看輸出。第一行執行測試。第三行 EE 顯示測試的結果。如果測試用例通過(guò),會(huì )得到 “.” 字符。如果測試用例產(chǎn)生錯誤,會(huì )看到 E。如果某個(gè)斷言不是 true,那么將看到 F。接下來(lái),可以看到所請求的全部測試都將完成,以及完成這些測試需要的時(shí)間。最后,將看到每個(gè)失敗的詳細原因。在這個(gè)示例中沒(méi)有表,這是有一定原因的,因為在測試數據庫中還沒(méi)有創(chuàng )建任何表。通過(guò)將開(kāi)發(fā)方案復制到測試環(huán)境,再重新運行測試,可以修復錯誤,如清單 10 所示: 清單 10. 復制方案,重新運行測試 > rake clone_schema_to_test (in /Users/batate/rails/trails) > ruby test/unit/trail_test.rb Loaded suite test/unit/trail_test Started . Finished in 0.038578 seconds. 1 tests, 1 assertions, 0 failures, 0 errors | 這樣更好。但是測試還是太簡(jiǎn)單,所以是構建一個(gè)真正的測試用例的時(shí)候了。請添加下面這個(gè)新測試用例 test_truth,如清單 11 所示: 清單 11. 添加測試用例 def test_truth assert true end def test_new trails = Trail.find_all Trail.new do |trail| trail.name = "Barton Creek" trail.description = "A little water in the Spring. You‘ll get wet." trail.difficulty = "medium" trail.save end bc = Trail.find_by_name("Barton Creek") assert_equal "medium", bc.difficulty assert_equal trails.size + 1, Trail.find_all.size end | 這個(gè)代碼驚人的緊湊。只需要鍵入上述代碼以及兩個(gè)斷言,就可以操縱持久模型。這種經(jīng)濟的投入正是腳本語(yǔ)言在其他環(huán)境中如此流行的原因。測試也是需要經(jīng)濟投入的地方。 現在可以運行測試用例,您將看到兩個(gè)新斷言顯示在測試報告中。使用 Ruby 時(shí),只需保存并編譯測試即可。清單 12 顯示了測試運行的結果: 清單 12. 測試結果 > ruby test/unit/trail_test.rb Loaded suite test/unit/trail_test Started . Finished in 0.038578 seconds. 1 tests, 1 assertions, 0 failures, 0 errors bruce-tates-computer:~/rails/trails batate$ ruby test/unit/trail_test.rb Loaded suite test/unit/trail_test Started .. Finished in 0.182043 seconds. 2 tests, 3 assertions, 0 failures, 0 errors | Fixture 和回滾 | Java mock 對象 在解決測試數據庫支持代碼的困擾時(shí),Java 開(kāi)發(fā)人員經(jīng)常使用 mock 對象而不是實(shí)際的數據庫代碼。Mock 對象設置起來(lái)比較難,通常難于理解,而且對于在數據庫環(huán)境中工作的代碼,也無(wú)法提供良好的理解。Ruby on Rails 支持不同的方式。 | | 有三個(gè)問(wèn)題影響了對數據庫支持代碼的測試。它們都與兩個(gè)特性有關(guān):性能和重復性。與內存中的操作相比較,數據庫調用的性能是非常低的。如果測試運行需要太長(cháng)時(shí)間,那么您可能就不想運行它們了。另一個(gè)問(wèn)題是一個(gè)測試用例對另一個(gè)測試用例的影響。因為數據庫調用在性質(zhì)上是持續的,所以要把一個(gè)測試在數據庫中的變化與另一個(gè)數據庫中的隔離開(kāi)。最后的問(wèn)題是前兩個(gè)問(wèn)題的組合。為了讓數據庫測試用例可重復而增加設置和拆卸的負擔時(shí)(為每個(gè)新的測試用例添加記錄、運行測試并刪除這些記錄),帶來(lái)的開(kāi)銷(xiāo)可能是讓人無(wú)法接受的。與這種開(kāi)銷(xiāo)相比,測試用例開(kāi)銷(xiāo)簡(jiǎn)直是小巫見(jiàn)大巫。 Ruby on Rails 用 fixture 和事務(wù)回滾來(lái)幫助解決這些問(wèn)題。在 Rails 中,一個(gè) fixture 就是一個(gè)包含測試用例數據的文件。在創(chuàng )建這個(gè)簡(jiǎn)單應用程序時(shí),同時(shí)還創(chuàng )建了一個(gè)開(kāi)發(fā)數據庫和一個(gè)測試數據庫。創(chuàng )建開(kāi)發(fā)數據庫是很正常的;但是您可能不想讓生產(chǎn)代碼和開(kāi)發(fā)環(huán)境共享同一個(gè)數據庫。而創(chuàng )建測試數據庫因為另一個(gè)原因也很重要。每個(gè)測試都在測試用例開(kāi)始時(shí)裝入 fixture 中的測試數據。然后,測試用例對數據庫進(jìn)行修改,并測試這些修改的結果。最后,Rails 回滾這些變化,將數據庫返回到測試方法運行之前的狀態(tài)。 現在要制作一個(gè)測試 fixture 并為它編寫(xiě)一個(gè)測試。請編輯 test/fixtures/trails.yml 文件,添加一個(gè)記錄,如清單 13 所示: 清單 13. 添加記錄 first: id: 1 name: "Emma Long" description: "A real bike breaker." difficulty: "hard" another: id: 2 name: "Bear Creek" description: "Too many downed trees." difficulty: "easy" | 清單 13 使用叫做 YAML 的語(yǔ)言,這個(gè)語(yǔ)言描述結構化的數據(請參閱 參考資料)。此文件對空格很敏感,所以該當用空格代替制表符并完全按原樣鍵入數據項時(shí),請確保刪除了所有尾部空格。 同樣,還要把這個(gè)測試用例添加到 trails_test.rb 中: def test_find assert_equal "Emma Long", Trail.find(1).name assert_equal "easy", Trail.find(2).difficulty end | 同樣,可以用 5 個(gè) passing 斷言運行這些測試。如果您愿意,還可以按名稱(chēng)引用每個(gè) fixture。例如,要根據名為 first 的 fixture 來(lái)創(chuàng )建對象,可以使用 Ruby 代碼 trails[:first]。讓 fixture 對所有測試用例或只對需要它們的測試用例可用,這極大地簡(jiǎn)化了創(chuàng )建或毀壞數據庫數據所需要的代碼。
在 Java 編程中測試 知道了測試在其他語(yǔ)言中如何發(fā)生,就可以改進(jìn)在 Java 平臺上進(jìn)行測試的方式。具體地說(shuō),使用這些想法中的一項或多項可以對測試產(chǎn)生顯著(zhù)而直接的影響: - 可以把測試用例的生成添加到任何現有代碼生成當中。Ruby on Rails 通過(guò)在默認情況下創(chuàng )建一些簡(jiǎn)單的測試用例來(lái)取得了巨大優(yōu)勢,您也可以這么做。
- 可以用事務(wù)-回滾技術(shù)讓數據支持的測試運行得更快。Spring 框架有一些現有的攔截器,可以讓這項技術(shù)易于使用。
- 實(shí)際上可以用動(dòng)態(tài)語(yǔ)言驅動(dòng)測試。Jython、Ruby 和 Groovy 是三個(gè)實(shí)際可能。
如果覺(jué)得愿意采用其他語(yǔ)言進(jìn)行測試,那么可以使用某種 JVM 語(yǔ)言,例如 JRuby(請參閱 參考資料)。JRuby 還沒(méi)有高級到可以運行 Ruby on Rails,但是它是 Java 應用程序卓越的測試平臺。只是作為嘗試,JRuby 的開(kāi)發(fā)人員 Charles O‘Nutter 提供了以下測試 EJB 的示例: 清單 14. 用 JRuby 測試 EJB 組件 require ‘test/unit‘ require ‘java‘ include_class "my.pkg.EJBHomeFactory" class TestMyBean < Test::Unit::TestCase def test_finder wh = EJBHomeFactory.widget_home w = wh.find_by_color("blue") assert_not_nil(w) end def test_widget wh = EJBHomeFactory.widget_home w = wh.find_by_name ("superWidget") assert_equal("blue", w.color) assert_equal(14, w.id) end end | 可以看到,用 Ruby 編寫(xiě)執行 Java 代碼的測試用例實(shí)際上非常容易。在這個(gè)示例中,Ruby 代碼發(fā)現一個(gè) EJB 組件,并為用戶(hù)返回的 bean 提供了一些斷言。測試用例當然比多數 Java 測試都容易,使用 Ruby 編寫(xiě)測試用例是一個(gè)獲得更高的生產(chǎn)率和速率的一種好方法。我還看到針對 Jython 或 Groovy 的類(lèi)似策略(請參閱 參考資料)。 第 2 部分將進(jìn)一步深入查看 Rails 的測試,包括運行更高層次測試(叫做功能測試和集成測試)的代碼。
參考資料 學(xué)習 獲得產(chǎn)品和技術(shù) - Ruby on Rails:下載開(kāi)放源碼的 Ruby on Rails Web 框架。
- Ruby:從 Ruby 項目的 Web 站點(diǎn)得到它。
- JUnit:開(kāi)始了 Java 平臺自動(dòng)測試熱浪的 Java 測試框架。
- TestNG:Java 開(kāi)發(fā)的下一代測試框架。
- JRuby:運行在 JVM 中的 Ruby 實(shí)現。
- Selenium:用于 Web 應用程序的集成測試框架。
- JUnitPerf:用來(lái)測試 JUnit 測試中的性能和伸縮性的 JUnit 測試修飾器集。
關(guān)于作者 | | | | Bruce Tate 居住在德克薩斯州的首府奧斯汀,他是一位父親,同時(shí)也是山地車(chē)手和皮艇手。他是三本 Java 暢銷(xiāo)書(shū)的作者,包括榮獲 Jolt 大獎的 Better, Faster, Lighter Java。最近他又出版了 Beyond Java 一書(shū)。他在 IBM 工作了 13 年,現在是 RapidRed 顧問(wèn)公司 的創(chuàng )始人,在這里他潛心研究基于 Java 技術(shù)和 Ruby on Rails 的輕量級開(kāi)發(fā)策略和架構。 | |