|
2006 年 6 月 27 日
Java ? 社區(qū)在推進(jìn)自動(dòng)單元測試方面已經(jīng)做了一項(xiàng)激動(dòng)人心的工作。越來越多的開放源碼框架支持在構(gòu)建項(xiàng)目的同時(shí)構(gòu)建自動(dòng)測試套件。Spring framework、JUnit、TestNG 和其他幾個(gè)框架的一些或全部靈感都來自自動(dòng)測試的思想。盡管如此,一些非 Java 語言和框架具有更多的測試動(dòng)機(jī)、更合適的測試工具和更統(tǒng)一的測試視角。通過觀察其他框架的測試方式,可以改進(jìn) Java 語言中的測試方式,甚至使用更合適的語言來測試 Java 代碼。這篇文章是關(guān)于在 Ruby on Rails 上進(jìn)行測試的兩篇文章中的第一篇,將介紹 Rails 單元測試的方式。
捕獲 bug
我還記得當(dāng)我第一次得到自動(dòng)測試的 bug 時(shí)的情況。在一次大會(huì)上,當(dāng)我做完叫做 Bitter Java 的演講之后,Mike Clark(Java 社區(qū)的自動(dòng)測試大師,性能調(diào)整工具 JUnitPerf 的作者(請參閱 參考資料),現(xiàn)在是 Ruby on Rails 專家)走近我。Mike 告訴我有一種方法可以通過自動(dòng)測試改進(jìn)我的演講。在那次大會(huì)的剩余時(shí)間里,我跟著他四處走,看到了我能看到的盡可能多的他的測試會(huì)議。我開始使用他推薦的技術(shù),并對把紅條(代表測試失?。┳兂删G條(代表測試通過)上了癮。自動(dòng)測試改變了我思考軟件開發(fā)的方式。
 |
關(guān)于本系列
在 跨越邊界 系列中,作者 Bruce Tate 提出了這樣一個(gè)觀點(diǎn):如今的 Java 程序員可以通過學(xué)習(xí)其他方法和語言得到很好的其他思路。自從 Java 明顯成為所有開發(fā)項(xiàng)目的最佳選擇以來編程前景已經(jīng)改變。其他的框架正影響構(gòu)建 Java 框架的方式,從其他語言學(xué)到的概念可以影響您的 Java 編程。您編寫的 Python(或 Ruby、Smalltalk ... )代碼可以改變您處理 Java 編碼的方式。
本系列為您介紹與 Java 開發(fā)根本不同,但也可以直接應(yīng)用于 Java 開發(fā)的編程概念和技術(shù)。在一些例子中,需要對技術(shù)進(jìn)行集成以利用它。在另外一些例子中,您將能夠直接應(yīng)用這些概念。單獨(dú)的工具不及其他語言和框架能夠影響 Java 社區(qū)中的開發(fā)人員、框架甚至基本方法的思想那么重要。
|
|
Java 社區(qū)絕對有自動(dòng)測試的 bug。坦白地說,我們別無選擇。競爭壓力迫使許多公司編寫越來越多的代碼,而測試人員越來越少,同時(shí)每個(gè)開發(fā)人員的又必須有更高的生產(chǎn)率。如果不進(jìn)行自動(dòng)測試,得到測試的內(nèi)容就會(huì)更少,面對現(xiàn)代應(yīng)用程序不斷增長的復(fù)雜性,較少的測試不是一個(gè)可行的選擇方案。
在過去十年中,我們已經(jīng)看到了對測試工具和技術(shù)的研究。JUnit 和 TestNG 都是支持自動(dòng)單元測試的優(yōu)秀工具,而且由日常的開發(fā)人員所驅(qū)動(dòng)。Selenium 是改進(jìn)集成和功能測試的工具。一套稱作敏捷技術(shù) 的新開發(fā)過程告訴人們要更加重視自動(dòng)測試,不要太多地依賴正式的設(shè)計(jì)工具,將它們作為提高質(zhì)量的惟一工具。Java 社區(qū)已經(jīng)走了很長的路。 (請參閱 參考資料,獲得這里討論的工具與技術(shù)的附加信息。)
其他編程社區(qū)也有 bug 工具, 其中一些社區(qū)使用的自動(dòng)測試要比 Java 開發(fā)人員還有多,他們使用自動(dòng)測試經(jīng)驗(yàn)有完全不同的原因:
- Smalltalk 程序員使用自動(dòng)測試已經(jīng)幾乎有 30 年的時(shí)間了,所以通過動(dòng)態(tài)類型化語言使用的一些技術(shù)更加先進(jìn)。
- 集成框架的開發(fā)人員的優(yōu)勢是了解框架元素的結(jié)構(gòu)和組合。有些框架,例如 Ruby on Rails,能夠生成測試用例,而且在默認(rèn)情況下提供測試特性。
- 具有高級元編程(metaprogramming)能力的語言,例如 Ruby and Lisp,允許使用其他語言不支持的一些測試技巧,例如更容易訪問 mock 對象。
在這一篇和下一篇文章中,將全面理解在 Ruby on Rails 集成開發(fā)框架中的測試方式。第 1 部分側(cè)重于測試模型對象,并提供一些從 Rails 獲得啟發(fā)的策略,可以用這些策略使 Java 單元測試更有效。第 2 部分把更多時(shí)間花在功能測試和集成測試上。作為 Java 程序員,您對一些概念可能比較熟悉,特別是在測試的時(shí)候,而其他一些概念可以拓展您的理解。
補(bǔ)漏
在這個(gè)系列的 前一期 中,了解了動(dòng)態(tài)類型化會(huì)帶來某些 bug 種類,靜態(tài)類型化語言將在編譯時(shí)捕捉到這些 bug。清單 1 的 Ruby 代碼片段包含四個(gè)不同的 bug,這四個(gè) bug 在運(yùn)行時(shí)之前都不會(huì)顯露出來:
清單 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,那么這類 bug 解決起來是小菜一碟,但是如果依賴解釋器,那么管理這些 bug 就困難得多。為了處理這些微妙的錯(cuò)誤,動(dòng)態(tài)語言的用戶長期以來一直依賴于自動(dòng)測試。在進(jìn)行測試的時(shí)候,比起其他語言,動(dòng)態(tài)語言及其集成環(huán)境在一般意義和特殊意義上都具有顯著的優(yōu)勢:
- 語言更簡潔。測試基本上是腳本編程,許多最好的腳本語言都是動(dòng)態(tài)類型化的。
- 集成環(huán)境支持的假設(shè)可以讓集成測試更容易,也可能更強(qiáng)大。在 Rails 環(huán)境中將看到一些示例。
- 動(dòng)態(tài)語言允許使用更松散的耦合,使一些測試格式更容易實(shí)現(xiàn)。
在了解動(dòng)態(tài)語言開發(fā)人員為什么這么熱衷于測試之后,現(xiàn)在是構(gòu)建一個(gè)需要一些真正測試的實(shí)際應(yīng)用程序的時(shí)候了。
構(gòu)建一個(gè)快速 Rails 應(yīng)用程序
為了進(jìn)展得快些,我采用了一個(gè)保存山地摩托車路線數(shù)據(jù)庫的 Rails 應(yīng)用程序。我將模型的幾個(gè)測試放在一起。如果想和我一起編寫代碼,那么所有需要的工具就是一個(gè)數(shù)據(jù)庫引擎(我使用的是 MySQL)和 Ruby on Rails 1.1 或更新版本(請參閱 參考資料)。第一步是創(chuàng)建 Rails 項(xiàng)目。在命令提示符下輸入 rails trails 命令,清單 2 顯示了命令和結(jié)果:
清單 2. 構(gòu)建 Rails 應(yīng)用程序
> 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 除了生成空項(xiàng)目什么都沒做,但是可以看到它正在為您工作。清單 2 創(chuàng)建的目錄中包含:
- 應(yīng)用程序目錄,包括模型、視圖和控制器的子目錄
- 單元測試、功能測試和集成測試的測試目錄
- 為測試而明確創(chuàng)建的環(huán)境
- 測試用例結(jié)果的日志
因?yàn)?Rails 是一個(gè)集成環(huán)境,所以它可以假設(shè)組織測試框架的最佳方式。Rails 也能生成默認(rèn)測試用例,后面將會(huì)看到。
現(xiàn)在要通過遷移創(chuàng)建數(shù)據(jù)庫表,然后用數(shù)據(jù)庫表創(chuàng)建新數(shù)據(jù)庫。請鍵入 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 和測試的更多內(nèi)容。遷移讓 Rails 開發(fā)人員可以在整個(gè)開發(fā)過程中處理數(shù)據(jù)庫表中不可避免的更改(請參閱 跨越邊界:研究活動(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
|
您需要?jiǎng)?chuàng)建和配置兩個(gè)數(shù)據(jù)庫:trails_test 和 trails_development。如果想把這個(gè)代碼投入生產(chǎn),那么還需要?jiǎng)?chuàng)建第三個(gè)數(shù)據(jù)庫 trails_production,但是現(xiàn)在可以跳過這一步。請用數(shù)據(jù)庫管理器創(chuàng)建數(shù)據(jù)庫。我使用的是 MySQL:
清單 5. 創(chuàng)建開發(fā)和測試數(shù)據(jù)庫
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 中的配置,以反映數(shù)據(jù)庫的優(yōu)先選擇。我的配置看起來像這樣:
清單 6. 將數(shù)據(jù)庫適配器添加到配置中
development:
adapter: mysql
database: trails_development
username: root
password:
host: localhost
test:
adapter: mysql
database: trails_test
username: root
password:
host: localhost
|
現(xiàn)在可以運(yùn)行遷移,然后把應(yīng)用程序剩下的部分搭建(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è)簡單的小程序生成了視圖和控制器,而且還生成了有助于測試用戶界面的功能性測試。
對 Rails 應(yīng)用程序進(jìn)行單元測試
現(xiàn)在是運(yùn)行一些測試的時(shí)候了。請看第一個(gè)測試,它已經(jīng)在 test/unit/trail_test.rb 中寫好了:
清單 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è)測試用例算不了什么,但您可以從中看出如何構(gòu)架測試代碼,而且自己的測試用例的模板也已經(jīng)就位。請運(yùn)行測試,如清單 9 所示(包括結(jié)果):
清單 9. 運(yùn)行第一個(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...
|
測試用例失敗,但是請看輸出。第一行執(zhí)行測試。第三行 EE 顯示測試的結(jié)果。如果測試用例通過,會(huì)得到 “.” 字符。如果測試用例產(chǎn)生錯(cuò)誤,會(huì)看到 E。如果某個(gè)斷言不是 true,那么將看到 F。接下來,可以看到所請求的全部測試都將完成,以及完成這些測試需要的時(shí)間。最后,將看到每個(gè)失敗的詳細(xì)原因。在這個(gè)示例中沒有表,這是有一定原因的,因?yàn)樵跍y試數(shù)據(jù)庫中還沒有創(chuàng)建任何表。通過將開發(fā)方案復(fù)制到測試環(huán)境,再重新運(yùn)行測試,可以修復(fù)錯(cuò)誤,如清單 10 所示:
清單 10. 復(fù)制方案,重新運(yùn)行測試
> 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
|
這樣更好。但是測試還是太簡單,所以是構(gòu)建一個(gè)真正的測試用例的時(shí)候了。請?zhí)砑酉旅孢@個(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)濟(jì)的投入正是腳本語言在其他環(huán)境中如此流行的原因。測試也是需要經(jīng)濟(jì)投入的地方。
現(xiàn)在可以運(yùn)行測試用例,您將看到兩個(gè)新斷言顯示在測試報(bào)告中。使用 Ruby 時(shí),只需保存并編譯測試即可。清單 12 顯示了測試運(yùn)行的結(jié)果:
清單 12. 測試結(jié)果
> 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ù)據(jù)庫支持代碼的困擾時(shí),Java 開發(fā)人員經(jīng)常使用 mock 對象而不是實(shí)際的數(shù)據(jù)庫代碼。Mock 對象設(shè)置起來比較難,通常難于理解,而且對于在數(shù)據(jù)庫環(huán)境中工作的代碼,也無法提供良好的理解。Ruby on Rails 支持不同的方式。
|
|
有三個(gè)問題影響了對數(shù)據(jù)庫支持代碼的測試。它們都與兩個(gè)特性有關(guān):性能和重復(fù)性。與內(nèi)存中的操作相比較,數(shù)據(jù)庫調(diào)用的性能是非常低的。如果測試運(yùn)行需要太長時(shí)間,那么您可能就不想運(yùn)行它們了。另一個(gè)問題是一個(gè)測試用例對另一個(gè)測試用例的影響。因?yàn)閿?shù)據(jù)庫調(diào)用在性質(zhì)上是持續(xù)的,所以要把一個(gè)測試在數(shù)據(jù)庫中的變化與另一個(gè)數(shù)據(jù)庫中的隔離開。最后的問題是前兩個(gè)問題的組合。為了讓數(shù)據(jù)庫測試用例可重復(fù)而增加設(shè)置和拆卸的負(fù)擔(dān)時(shí)(為每個(gè)新的測試用例添加記錄、運(yùn)行測試并刪除這些記錄),帶來的開銷可能是讓人無法接受的。與這種開銷相比,測試用例開銷簡直是小巫見大巫。
Ruby on Rails 用 fixture 和事務(wù)回滾來幫助解決這些問題。在 Rails 中,一個(gè) fixture 就是一個(gè)包含測試用例數(shù)據(jù)的文件。在創(chuàng)建這個(gè)簡單應(yīng)用程序時(shí),同時(shí)還創(chuàng)建了一個(gè)開發(fā)數(shù)據(jù)庫和一個(gè)測試數(shù)據(jù)庫。創(chuàng)建開發(fā)數(shù)據(jù)庫是很正常的;但是您可能不想讓生產(chǎn)代碼和開發(fā)環(huán)境共享同一個(gè)數(shù)據(jù)庫。而創(chuàng)建測試數(shù)據(jù)庫因?yàn)榱硪粋€(gè)原因也很重要。每個(gè)測試都在測試用例開始時(shí)裝入 fixture 中的測試數(shù)據(jù)。然后,測試用例對數(shù)據(jù)庫進(jìn)行修改,并測試這些修改的結(jié)果。最后,Rails 回滾這些變化,將數(shù)據(jù)庫返回到測試方法運(yùn)行之前的狀態(tài)。
現(xiàn)在要制作一個(gè)測試 fixture 并為它編寫一個(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 的語言,這個(gè)語言描述結(jié)構(gòu)化的數(shù)據(jù)(請參閱 參考資料)。此文件對空格很敏感,所以該當(dāng)用空格代替制表符并完全按原樣鍵入數(shù)據(jù)項(xiàng)時(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 斷言運(yùn)行這些測試。如果您愿意,還可以按名稱引用每個(gè) fixture。例如,要根據(jù)名為 first 的 fixture 來創(chuàng)建對象,可以使用 Ruby 代碼 trails[:first]。讓 fixture 對所有測試用例或只對需要它們的測試用例可用,這極大地簡化了創(chuàng)建或毀壞數(shù)據(jù)庫數(shù)據(jù)所需要的代碼。
在 Java 編程中測試
知道了測試在其他語言中如何發(fā)生,就可以改進(jìn)在 Java 平臺(tái)上進(jìn)行測試的方式。具體地說,使用這些想法中的一項(xiàng)或多項(xiàng)可以對測試產(chǎn)生顯著而直接的影響:
- 可以把測試用例的生成添加到任何現(xiàn)有代碼生成當(dāng)中。Ruby on Rails 通過在默認(rèn)情況下創(chuàng)建一些簡單的測試用例來取得了巨大優(yōu)勢,您也可以這么做。
- 可以用事務(wù)-回滾技術(shù)讓數(shù)據(jù)支持的測試運(yùn)行得更快。Spring 框架有一些現(xiàn)有的攔截器,可以讓這項(xiàng)技術(shù)易于使用。
- 實(shí)際上可以用動(dòng)態(tài)語言驅(qū)動(dòng)測試。Jython、Ruby 和 Groovy 是三個(gè)實(shí)際可能。
如果覺得愿意采用其他語言進(jìn)行測試,那么可以使用某種 JVM 語言,例如 JRuby(請參閱 參考資料)。JRuby 還沒有高級到可以運(yùn)行 Ruby on Rails,但是它是 Java 應(yīng)用程序卓越的測試平臺(tái)。只是作為嘗試,JRuby 的開發(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 編寫執(zhí)行 Java 代碼的測試用例實(shí)際上非常容易。在這個(gè)示例中,Ruby 代碼發(fā)現(xiàn)一個(gè) EJB 組件,并為用戶返回的 bean 提供了一些斷言。測試用例當(dāng)然比多數(shù) Java 測試都容易,使用 Ruby 編寫測試用例是一個(gè)獲得更高的生產(chǎn)率和速率的一種好方法。我還看到針對 Jython 或 Groovy 的類似策略(請參閱 參考資料)。
第 2 部分將進(jìn)一步深入查看 Rails 的測試,包括運(yùn)行更高層次測試(叫做功能測試和集成測試)的代碼。
參考資料
學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
- Ruby on Rails:下載開放源碼的 Ruby on Rails Web 框架。
- Ruby:從 Ruby 項(xiàng)目的 Web 站點(diǎn)得到它。
- JUnit:開始了 Java 平臺(tái)自動(dòng)測試熱浪的 Java 測試框架。
- TestNG:Java 開發(fā)的下一代測試框架。
- JRuby:運(yùn)行在 JVM 中的 Ruby 實(shí)現(xiàn)。
- Selenium:用于 Web 應(yīng)用程序的集成測試框架。
- JUnitPerf:用來測試 JUnit 測試中的性能和伸縮性的 JUnit 測試修飾器集。
關(guān)于作者
 |
|

|
 |
Bruce Tate 居住在德克薩斯州的首府奧斯汀,他是一位父親,同時(shí)也是山地車手和皮艇手。他是三本 Java 暢銷書的作者,包括榮獲 Jolt 大獎(jiǎng)的 Better, Faster, Lighter Java。最近他又出版了 Beyond Java 一書。他在 IBM 工作了 13 年,現(xiàn)在是 RapidRed 顧問公司 的創(chuàng)始人,在這里他潛心研究基于 Java 技術(shù)和 Ruby on Rails 的輕量級開發(fā)策略和架構(gòu)。
|
|