鐵道上的礦工

pageicon 星期一 十一月 10, 2008

Rails 2.2.0 I18n 的使用筆記


[Read More]
pageicon 星期日 十月 26, 2008

在你的Rails App中管理Plugins

這篇是我讀The Rails Way時的筆記,主要是如何管理plugins。首先是Rails內建的script/plugin。

Rails內建的Plugin Script
使用script/plugin命令應該是最常的吧。我之前唯一會的也只有這個:
$ cd projects/plugin-test/
$ script/plugin install restful_authentication
+ ./README
+ ./Rakefile
+ ./generators/authenticated/USAGE
+ ./generators/authenticated/authenticated_generator.rb
+ ./generators/authenticated/templates/activation.html.erb
+ ./generators/authenticated/templates/authenticated_system.rb
./generators/authenticated/templates/authenticated_test_helper.rb
...


注意喔,script/plugin必須在你的Rails app的根目錄中執行喔。
這樣就安裝了restful_authentication 這個plugin到vendor/plugins目錄中了。可是script/plugin不是只有下載plugin這個功能喔,接下來我會介紹其他的commands。

script/plugin list
這個command會列出plugin所知道的repository url中可以使用的plugin,當你執行
$script/plugin install restful_authentication
script/plugin也是檢查它所知道的repository url中是否有plugin叫做restful_authentication。

script/plugin sources
這個command會列出plugin所知道的repository url。其實這些url就放在~/.rails-plugin-sources。(在Mac OS X或Linux中,至於Windows我就不知道了耶。)


script/plugin sources [url [url2 [...]]]
這個命令可以讓你新增plugin repository url。如果這個命令執行失敗,表示這個url已經存在了。

script/plugin unsources [url [url2 [...]]]
這個命令可以讓你移除plugin repository url。

script/plugin discover [url]

這個命令預設會去爬http://wiki.rubyonrails.org/rails/pages/Plugins頁面中有 'plugin'字串的http url或svn url。你也可以提供自己的頁面,那他就會去爬你提供的頁面。

script/plugin install [plugin]

這個命令可以讓你下載一個plugin到vendor/plugins中。
除了只給plugin名稱,你也可以直接寫出plugin的url,這樣這個plugin就不一定要是~/.rails-plugin-sources中有紀錄的url了。

script/plugin remove [plugin]
這個命令可以讓你從vendor/plugins移除一個plugin。如果這個plugin有uninstall.rb,那在移除之前會先執行。

script/plugin update [plugin]
這個命令可以讓你將vendor/plugins中的plugin升級到最新的版本。

Subversion與script/plugin
當我們使用script/plugin install這個command,而沒有其他選項時,它會取回一個plugin的複製,但不會幫你check out到你的Repository中。

一個比較好的方式是使用Subversion來存放你的程式碼,並且有額外的訊息來紀錄你所用到的plugins的目前版本,與哪裡可以取得這些plugins。這個額外的訊息可以幫助你自動將你所用到的plugins升級到最新的版本。你可以用-o 這個選項來達到這個目的:

$ script/plugin install -o white_list
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/test
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/test/white_list_test.rb
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/Rakefile
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/init.rb
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/lib
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/lib/white_list_helper.rb
A    /home/weijen/projects/plugin-test/vendor/plugins/white_list/README
取出修訂版 3183.


在上面這個例子中,其實white_list並沒有存到我的Repository中。

script/plugin update
如果你的plugin是用script/plugin install -o來安裝的,那update這個command就會自動幫你的plugins升級到最新版本。你也可以用-r這個選項來選擇要升級到哪個版本。

SVN Externals

使用install -o這個命令,在配置我們的程式到Server上時會有一些問題。因為這些plugin只存在你的工作目錄中,他們跟你的project並沒有任何的連結。因此你需要在你將配置的server上手動安裝這些plugins,聽起來就不是好方法。還好我們有svn:externals。

屬性svn:externals會在你check out或update你的程式碼的時候,同步check out或update這些plugin,不過不是你程式碼的repository,而是這些plugins各自的repository。我們可以利用script/plugin install -x來做到:

$ script/plugin install -x restful_authentication
A    /home/weijen/projects/plugin-test/vendor/plugins/restful_authentication/Rakefile
A    /home/weijen/projects/plugin-test/vendor/plugins/restful_authentication/lib
A    /home/weijen/projects/plugin-test/vendor/plugins/restful_authentication/lib/restful_authentication
A    /home/weijen/projects/plugin-test/vendor/plugins/restful_authentication/lib/restful_authentication/rails_commands.rb
A    /home/weijen/projects/plugin-test/vendor/plugins/restful_authentication/install.rb
...


我們可以利用svn propget svn:externals這個SVN command來看看發生了甚麼事:
$ svn propget svn:externals vendor/plugins/ restful_authentication
vendor/plugins - restful_authentication        http://svn.techno-weenie.net/projects/plugins/restful_authentication

鎖定在某一個版本
很多時候,我們並不需要plugins升級到最新的版本,那我們可以利用-r這個選項來指定某一個版本號碼。

使用Piston


Piston是用來管理你的專案中的vendor目錄中的函式庫的版本,比起直接使用Subversion,它可以節省你的時間,並減少錯誤。
不同於使用svn的svn:externals屬性,Piston會複製一份副本放進你的repository中。但是Piston會保存一份資料,存放跟svn有關的原始碼與修訂版號有關的訊息。

安裝

安裝Piston就只要一行指令:
sudo gem install piston

導入一個函式庫
import命令要Piston導入一個函式庫。例如我要使用EdgeRails,我就從ror的官方svn導入最新的Rails。
$piston import http://dev.rubyonrails.org/svn/rails/trunk vendor/rails

Piston並不會幫你放到你的repository中,你必須自己check in。
Piston不像是script/plugin,會預設放到vendor/plugins中,你需要指定目的地。預設是你目前的目錄。

轉變現有的函式庫
如果你已經有些函式庫是使用svn:externals,你可以在你專案的根目錄,使用convert命令來轉換。

Updating
如果你要把你所使用的函式庫升級到最新的版本,使用update命令。
$piston update vendor/plugins/white_list/

鎖定與解除鎖定某一個修訂版本
要鎖定某一個函式庫,避免讓其他同事升級了,你可以使用lock命令,那要解除鎖定就使用unlock命令。

 

 

pageicon 星期四 三月 27, 2008

Ruby-GetText找不到.html.erb中的msgid

我照著Ruby-GetText官網上的toturial來練習一下l10n/i18n,其實都還蠻容易的,
配合poEdit來作翻譯,更是方便。
可是我遇到一個問題,就是在*.html.erb中的msgid都會找不到。
看了source code才發現,原來是與Rails 2.0不相容的問題,但也很容易解決。

我的系統是Fedora 8
Ruby 1.8.6
Rails 2.0.2
Ruby-GetText 1.10.0
問題就在於ErbParser這個module,他不會去找html.erb這個類型的檔案。所以只要在
/usr/lib/ruby/gem/1.8/gem/gettext1.10.0/lib/gettext/paser/erb.rb
中找到這段程式,並加上紅字那一段:
  1. module ErbParser
  2. @config = {
  3. :extnames => ['.rhtml',.html.erb']
  4. }
這樣就可以順利找到了。
pageicon 星期一 二月 11, 2008

DRb(Distributed Ruby)簡介

甚麼是DRb:

DRb是Distributed Ruby的縮寫,它是一套函式庫讓你可以透過TCP/IP來與遠端Ruby物件傳送接收訊息。就如同Java 的RMI一樣。在Ruby的官網中,則是使用dRuby的縮寫。就如同絕大部分的網路架構,DRb也有兩個主要的部份:Server端/Client端。

Server端:

在伺服端的Process是由一個DRb::DRbServer的實例(instance)來負責的。他可以重組來自client的呼叫(Method call/也是訊息),並將結果回傳給client。DRb不需要你另外提供介面,或者mixin任何額外的module,你就可以進行遠端呼叫了。
    Server 的責任:
  1.     啟動一個TCP server接口(socket),並聽取一個port。
  2.     連接一個物件與DRb server的實例(instance)。
  3.     接收與回應clients的要求。
  4.     選擇性的提供存取控制服務。


Client端:

server端的物件,在client端都當作是DRb::DRbObject的實例。這個物件(DRb::DRbObject的實例)就像是Server端物件的代理一般,呼叫方法時,會將訊息傳給Server端的物件來負責執行。而在Client端,我們不需要另外提供介面,所有的動作都是run-time執行的。
    Client 的責任:

  1.     建立與DRb server的連線。
  2.     連接區域的物件與遠端的DRb物件。
  3.     傳送訊息給Server以及接收回應。

簡單範例

我們要呼叫遠端的物件,我們總要用一些方法來跟遠端取得溝通管道吧,而在DRb中,這個管道就是URI。每一個DRb::DRbServer的實例都會連結一個URI,就像是druby://example.com::8899。而DRb::DRbServer的實例還會連結一個物件,這個物件就是Server端所提供的前端物件(front object),當client端的DRb::DRbObject物件連結過來時,就會成為這個前端物件的代理(其實就是傳這個前端物件的參考給client端的DRb::DRbObject物件)。我們來看看Server端的範例:

  1. #
  2. #Server.rb
  3. #
  4. require 'drb'
  5. DRb.start_service(druby://localhost:8899, Array.new)
  6. puts DRb.uri
  7. DRb.thread.join

就是這麼簡單,只有四行,我們一行行解釋一下。第4行應該大家都知道,就只是把DRb這個函式庫包含進來。那第5行的DRb.start_service就是啟動Server,其中第一個參數就是uri,而第二個參數就是前端物件,在這裡就只是一個簡單的Array物件。第6行只是單純show出Server端的uri。第7行則是啟動這個Server的thread,如此可以保證這個Server Process不會停止,除非人為中斷他(例如ctrl+C)。
我怎麼都沒看到DRb::DRbServer呢?其實DRb.start_service就是幫你新增一個DRbServer的實例,可是在一個系統中,一次只有一個DRbServer物件是primary server,所以如果有其他的DRbServer物件已經存在,最後新增的DRbServer物件還是會成為primary server。而後面的DRb.uri、DRb.thread的函式呼叫都是針對這個最後新增的DRbServer物件。

接下來我們來看看Client端的範例:

  1. #
  2. #Client:
  3. #
  4. require 'drb'
  5. DRb.start_service
  6. remote_array=DRbObject.new(nil,  druby://localhost:8899)
  7. puts remote_array.size
  8. remote_array<<1
  9. puts remote_array.size


第5、6行是啟動一個連線,並建立一個DRbObject物件,來成為Server端的前端物件的代理,接下來的幾行就是直接對這個遠端物件做操作了。看官如果有興趣可以玩玩看。

雙向溝通與DRbUndumped

也許各位會懷疑,為什麼在Client端也要一個DRb.start_service呀?其實Client也可以是一個Server,ㄏㄏ,有沒有很混亂。我們用一個計算兩點距離的例子來說明:

  1. #
  2. #Server:
  3. #
  4. require 'drb'
  5. class DistCalc
  6.     def find_distance(p1, p2)
  7.         Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
  8.     end
  9. end
  10. DRb.start_service(nil, DistCalc.new )
  11. puts DRb.uri
  12. Drb.thread.join

這個例子跟前面的例子其實差不多,不一樣的就只有之前我們用的是ruby提供的Array物件,而這次用的是我們自己寫的DistCalc的物件。而在start_service中的第一個參數(uri參數)如果是nil,那DRb會在localhost自己找一個適合的port來用。
接下來我們看Client端的例子:

  1. #
  2. #Client:
  3. #
  4. require 'drb'
  5. Point=Struct.new 'Point', :x, :y
  6. class Point
  7.     include DRbUndumped
  8.      
  9.     def to_s
  10.         "(#{x}, #{y})"
  11.     end
  12. end
  13. DRb.start_service
  14. dist_calc=DRbObject.new(nil, ARGV.shift )
  15. p1=Point.new 3,5
  16. p2=Point.new 8,9
  17. puts "The distance between #{p1} and #{p2} is #{dist_calc.find_distance(p1, p2)}"

看起來比較長了,其實跟前面的例子還是差不多的,主要的差別在於我們的Point類別中,mixin了一個DRbUndumped module,這��當我們呼叫Server端的find_distance(p1,p2)時,p1,p2這兩個物件並不需要將整個物件傳遞到Server端,而是就像client端擁有Server端前端物件的參考一樣,Server端擁有的也只是p1、p2的參考,當Server端要呼叫p1.x時,也是連回client端來取得p1.x的值,並將結果傳回Server端。
DRbUndumped讓物件只需要一份實例,而不需要將物件整個傳遞到Server端,然後重新建立。

同步問題

因為DRb內部用的是thread,我們同樣要考慮thread safe的問題,其實Ruby提供了很多方法來解決這個問題,各位可以參考Programming Ruby 2rd的第11章,那這裡我舉個簡單的例子:

  1. calss FancyCalculator
  2.     def initialize
  3.         @mutex=Mutex.new
  4.     end
  5.     def calc(inval)
  6.         @mutex.synchronize do
  7.             @inval=inval
  8.             intermediary=first_calc
  9.             nextval=second_calc
  10.             return nextval
  11.         end
  12.     end
  13. end ­

­這跟DRb有甚麼關係?對,沒甚麼關係,但是DRb中真的要注意這個thread safe的問題,所以我寫了下來,至於甚麼是thread safe,還有哪些方法可以做到,就要各位自己去翻書了,我就不多說了。

安全問題

既然是提供可以遠端存取,那安全的問題就一定存在,大家都知道,網路上很多壞人的。

ACL

Drb可以透過ACL類別來進行存取控制,每一個DRbServer的物件一定都連結一個ACL的物件,這個ACL物件可以決定哪些IP可以或不可以對Server進行存取,這裡有一個簡單的範例:­

­
  1. #
  2. #security issus
  3. #
  4. if __FILE__==$0
  5.     acl=ACL.new(%w(deny all
  6.                allow 192.168.1.*
  7.                allow localhost))
  8.     DRb.install_acl(acl)
  9.     DRb.start_service(druby://localhost:8899, Hash.new)
  10.     puts DRb.uri
  11.     Drb.thread.join

­

有關ACL的設定與使用方式可以參考:http://www.ruby-doc.org/core/classes/ACL.html 

可怕的eval()

想像下面的code:

  1. ro=DRbObject::new_with_uri(“druby://localhost:8899”)
  2. class << ro
  3.     undef :instance_eval #強迫把呼叫傳給遠端物件
  4. end
  5. ro.instance_eval(“' rm -rf * ' “)

會砍掉Server端的檔案,這當然是不行的,要解決這個問題,我們可以透過一個ruby的變數$SAFE來解決,當我把$SAFE設為 1 時,就會禁止eval()相關的呼叫。

後話

我會需要了解DRb是因為在我的Rails app中需要提供一個reset的功能,這個reset要執行很久的時間,我不能在controller直接執行他,因為web server會等太久而以為連線斷了,之前我是直接用exec來執行一個我寫的script,可是這樣我又不能monitor執行的狀態,後來我發現原來有一個plugin叫做BackgroundDRb可以解決這個問題,反正要看BackgroundDRb,那就也把DRb給搞熟一點,會比較容易了解,這裡也預告一下,下一編就是會簡介有關BackgroundDRb喔。

Reference:

Rdoc:  http://www.ruby-doc.org/core/classes/DRb.html
Intro to Drb : http://www.chadfowler.com/ruby/drb.html
Introduction to Distributed Ruby (Drb) : http://segment7.net/projects/ruby/drb/introduction.html

pageicon 星期五 一月 25, 2008

Testing Your Model

Rails Test Environment
要做Model的測試,第一件事就是建立測試資料庫,首先你要修改在config/database.yml中的資料:
test:  #<--專門針對測試用的喔,強烈建議不要跟development/production共用
  adapter: mysql
  database: messages_test
  username: root
  password:  xxxx
  socket: /var/lib/mysql/mysql.sock
並且手動新增messages_test這一個資料庫。
接下來我們就要建立Schema,還好我們有rake,rake提供兩個指令:
  1. rake db:test:clone_structure          #依照目前development mode的db schema來建立table。
  2. rake db:test:clone                              #依照目前執行環境的db schema來建立tables。

說真的,我不太知道這兩個差在哪裡,一般來說我都是使用第一個來建立我的測試資料庫。

Fixtures
當你要進行Model的Unit test時,你會需要一些固定的資料,沒有人會希望這些固定的資料必須在我每次進行測試前都要手動去Insert到資料庫中吧,這個時候你就需要Fixtures了。
Fixtures讓你把要測試的資料,以YAML, CSV等格式的文字檔儲存,當你需要進行測試時,就可以用簡單的指令,將這些YAML/CSV文字檔的資料在測試資料庫中重建,以確保你的測試是正確的。
YAML
在fixtures中,用YAML格式來表示你要建立的資料似乎是最常見的。YAML格式的檔案的副檔名都是.yml。我們看一個簡單的例子:
  1. #範例三:message.yml
  2. #我是註解
  3. one:  #<--[1]
  4.   id: 1  #<-- [2]
  5.   message: 我是第一條訊息。
  6.   status: 0
  7.   popup: false
  8.   created_at: 2008-01-23 20:30:00
  9. two:
  10.   id: 2
  11.   message: 我是第二條訊息。
  12.   status: 0
  13.   popup: false
  14.   created_at: 2008-01-23 20:30:00

[1] 每一個fixture(其實就是每一筆將要存到測試資料庫的資料),都會有一個名稱(在範例一中是one, two)。然後用一個冒號( : )來區隔名稱與資料內容。有一點一定要注意喔,冒號後面至少要有一個空白字元,這是規定,不要問我為什麼。有關YAML的格式可以參考http://en.wikipedia.org/wiki/YAML。
[2] 每一個column也是有name/value pair,同樣也以冒號來區隔。

CSV (Comma Separated Value)

fixtures中也可以接受CSV格式的檔案。顧名思義,CSV格式的內容是以逗點( , )來區隔資料,檔案是以.csv為副檔名,與yaml格式的檔案一樣,都是放在test/fixtures目錄中。CSV格式的檔案看起來像是這樣。

  1. #範例四:我是CSV檔案
  2. #message.csv
  3. id, message, status, popup, created_at #<--[1]
  4. 1, “Pull your teeth, I will”, 0, false, 2008-01-23 20:30:00  #<--[2]
  5. 2, I like to say “”Ho! Ho! Ho!””, 0, false, 2008-01-23 20:30:00 #<--[3]
  6. 3, , 0, ,  2008-01-23 20:30:00   #<--[4]
[1] 第一行定義的是每一欄的名稱(column name),也是以逗點來做區隔。
[2] 如果在資料中有需要用到逗點,那要用雙引號把整個字串包起來。
[3] 如果字資料中有需要用到雙引號,那就要用連續兩個雙引號來表示。
[4] 如果要表示nil,那就直接不填值,並用逗號區隔就好了。以這行來說,在資料庫中的資料會是這樣:
        :id=>3
    :message=>nil
    :status=> 0
    :popup=>nil
    :created_at=> 2008-01-23 20:30:00


另外,在csv中的每一筆資料的fixture name,都是由系統內定的,他的定名規則是Model_Name+”-”+counter。以範例四來說,會產生三個fixture name,分別是message-1, message-2, message-3。 如果你有舊的資料庫資料,或者把資料存在Excel中的話,都有工具可以轉成CSV格式的檔案,那你就可以很容易得來進行測試了。
Single File
Fixtures其實還可以接受一種資料格式,就是把一筆資料(row)存到一個檔案之中,那你就把這些檔案,放在test/fixture/model_name中。不過很明顯的,官方並不建議用這個方式,既然不建議,那我就不多介紹了。
Test Model Using Fixtures
那讓我們來看看怎麼使用Fixtures吧,我們利用範例三的資料來Message這個Model:
  1. #範例五
  2. #message_test.rb
  3. require File.dirname(__FILE__) + '/../test_helper'
  4. class MessageTest < Test::Unit::TestCase
  5.   fixtures :message  # <--[1]
  6.   # Test count method
  7.   def test_count # <--[2]
  8.     assert_equal 2, Message.count
  9.   end
  10.  
  11.   def test_destroy #<--[3]
  12.     assert Message.destroy_all
  13.   end
  14.  
  15.   #Test get all messages
  16.   def test_get_all_messages  
  17.     assert_equal "我是第一條訊息。    我是第二條訊息。", Message.get_all_messages
  18.   end
  19. end
然後在Project的根目錄中,執行命令:
$ ruby test/unit/message_test.rb --verbose
Loaded suite test/unit/message_test
Started
test_count(MessageTest): .
test_destroy(MessageTest): .
test_get_all_messages(MessageTest): .   #<--[4]
 
Finished in 0.02373 seconds.
 
3 tests, 3 assertions, 0 failures, 0 errors
 

說明如下:
[1] 你必須使用fixtures 函式,並傳入你要測試的model的名稱,你也可以傳入多個model的名稱,只要用逗號區隔就可以了,例如:
    fixtures :messages, :users
[2] 一般的測試寫法,就跟一般的Ruby unit test一樣。
[3][4] 不知道各位有沒有發現,我在[3]中已經把所有的資料都刪除了,可是在[4]中我還是可以做測試,而且成功,那表示在每一個測試中,fixtures都會幫我們把資料庫中的資料回復回來,以確保我們每一個測試的結果不會受到前次測試的影響,是不是很方便。

pageicon 星期一 一月 21, 2008

Ruby Unit Testing

為什麼要寫Unit Test應該不用再多說了吧,上Google查Unit Test一定有上萬筆的資料,幾乎所有的語言都有Unit Test Tool,Ruby當然也不例外。Ruby預設安裝的是由Nathaniel Talbott(我不知道怎麼念,可以請會念的人幫我標一下kk音標嗎?)所作的Test::Unit Framework。

Test::Unit framework提供3個主要的功能:

  • 讓你可以進行獨立測試。
  • 讓你結構化你的測試。
  • 讓你能執行你的測試程式。


How to Write Ruby Unit Test
Test::Unit framework提供了很多的assertion method,讓你進行獨立測試,這些assertion methods是定義在Test::Unit::Assertions中,我先用範例一來說明使用方式,這個例子是阿拉伯數字轉成羅馬數字的類別與測試類別(這其實是Programming Ruby 2nd的範例啦):

  1. #範例一
  2. #lib/roman.rb
  3. class Roman
  4.   MAX_ROMAN = 4999
  5.   def initialize(value)
  6.     if value <= 0 || value > MAX_ROMAN
  7.       fail "Roman values must be > 0  and <= #{MAX_ROMAN}"
  8.     end
  9.     @value=value
  10.   end
  11.  
  12.   FACTORS = [
  13.           ["m",1000], ["cm",900], ["d",500],
  14.           ["cd",400],["c",100],["xc",90],
  15.           ["l",50],["xl",40],["x",10],
  16.           ["ix",9],["v",5],["iv",4],["i",1]
  17.         ]
  18.  
  19.   def to_s
  20.     value = @value
  21.     roman=""
  22.     
  23.     for code, factor in FACTORS
  24.       count, value = value.divmod(factor)
  25.       roman << (code * count)
  26.     end
  27.     roman
  28.   end
  29. end

  30. #測試檔案
  31. #tc_roman.rb
  32. $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
  33. require "test/unit"  # [1]
  34. require "roman"
  35. class RomanTest < Test::Unit::TestCase  #[2]
  36.   NUMBERS = [
  37.       ["ix",9],["v",5],["iv",4],["iii",3],["ii",2],["i",1]
  38.   ]
  39.   def test_to_s  # [3]
  40.     NUMBERS.each do |code, value|
  41.       assert_equal(code, Roman.new(value).to_s)  # [4]
  42.     end
  43.   end
  44.  
  45.   def test_range
  46.     assert_raise(RuntimeError) {Roman.new(0)}  # [5]
  47.     assert_nothing_raised() {Roman.new(1)}  # [6]
  48.     assert_nothing_raised() {Roman.new(Roman::MAX_ROMAN)}
  49.     assert_raise(RuntimeError) {Roman.new(Roman::MAX_ROMAN+1)}
  50.   end
  51. end
說明如下:
[1]單元測試都必須require 'test/unit'。
[2]單元測試類別必須是Test::Unit::TestCase的子類別。
[3]每一個包含assertion的函式,都必須是test_開頭。
[4]assert_equal應該是我最常用的assertioin了,它會幫我們比對我們預期的值與程式執行的回傳值是否相同,它的API定義:
    assert_equal(expected, actual, [message=nil])
第一個參數是我們預期程式執行的結果,第二個參數是程式執行的結果,第三個參數是當assertion failure時,我們要秀出的訊息。
[5][6] assert_raise/ assert_nothing_raise則是檢查是否會傳出Exception。

現在你只需要到tc_roman.rb所在的目錄下執行ruby tc_roman.rb,就可以看到你的測試結果了。
真的那麼簡單?很簡單~。(這是廣告詞)

Method setup() and teardown()

在測試類別中,如果你有一個稱為setup()的函式,那它將會在每一個要執行的測試函式執行前先執行,而如果你有定義teardown()函式,它將會在每個測試程式執行完後執行。我們用範例二來說明:
  1. #範例二
  2. class TestPlaylistBuilder < Test::Unit::TestCase
  3.   def setup # [1]
  4.     @db=DBI.connect('DBI:mysql:playlists')
  5.     @pb=PlaylistBuilder.new(db)
  6.   end
  7.  
  8.   def teardown # [2]
  9.     @db.disconnect
  10.   end
  11.  
  12.   def test_empty_playlist
  13.     assert_equal([], @pb.playlist())
  14.   end
  15.  
  16.   def test_artist_playlist
  17.     @pb.include_artist("krauss")
  18.     assert(@pb.playlist.size>0, "Playlist shouldn't be empty")
  19.     @pb.playlist.each do |entry|
  20.       assert_match(/krauss/i, entry.artist)
  21.     end
  22.   end
  23.  
  24.   def test_title_playlist # [3]
  25.     @pb.include_title('midnight')
  26.     assert(@pb.playlist.size>0, "Playlist shouldn't be empty")
  27.     @pb.playlist.each do |entry|
  28.       assert_match(/midnight/i, entry.title)
  29.     end
  30.   end # [4]
  31. end

 說明如下:
[1] 在TestPlaylistBuilder類別中test_開頭的測試函式執行前都會先執行這個setup()函式,建立與資料庫的連線,取出playlists中的資料,並instance一個PlaylistBuilder物件。
[2] 在TestPlaylistBuilder類別中test_開頭的測試函式執行後都會執行這個teardown()函式,中斷與資料庫的連線。
[3] 以test_title_playist為例,我們要測試的物件@pb已經在setup()中準備好了,而且不會因為@pb在test_artist_playlist函式中已經執行過,所以資料有問題,因為setup()會在每次執行test_xxx前都執行一次,確保資料正確。
[4] 以test_title_playist為例,測試執行結束了,應該與資料庫中斷連線,所以執行teardown()。

Structure(Programming Ruby的建議)
我們以範例一來說,它的檔案結構應該長成這樣:

roman/
    lib/
        roman.rb
        other files
    test/
        tc_roman.rb
        other tests
在這樣的結構下,test file要怎麼找到要被測試的類別呢,以Programming Ruby 2nd 所建議的結構,他建議在每個測試程式前面放入一行:
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
而接下來就可以直接 require 'roman' 了。 其中$:是將Load Path載入,然後將../lib放到Load Path的第一位,如此一來,當我們require 'roman' 時,測試程式在自己的目錄中找不到,就會先到../lib中尋找。

如果你要測試好幾個檔案,例如說整合測試,那該怎麼辦?在Ruby中,這是一件很簡單的事情,就是你把你要測試的測試檔案都include進來就好了。而這個包含很多測試檔案的檔案我們稱為Test Suite。

在這裡我們應該談一下檔案的命名規則了。Ruby Test::Unit的作者Nathaniel Talbott定義了一個命名規則(非強制的),如果這個檔案是單純的Test Case,就以tc_開頭;如果這個檔案是Test Suite就以ts_開頭(看起來,Ruby on Rails就沒有依照這個規則)。

Running Test
以範例一來說,我們每次執行ruby tc_roman.rb時,裡面的兩個測試都做了,那我們可不可以只做test_to_s的測試嗎?當然可以,我們在project的根目錄中執行命令:
$ ruby test/tc_roman.rb --name test_to_s --verbose
Loaded suite test/tc_roman
Started
test_to_s(RomanTest): .

Finished in 0.000873 seconds.

1 tests, 6 assertions, 0 failures, 0 errors
--name 選項也可以使用regular expression喔,例如說:
$ ruby test/tc_roman.rb --name '/test_t/' --verbose
Loaded suite test/tc_roman
Started
test_to_s(RomanTest): .

Finished in 0.001016 seconds.

1 tests, 6 assertions, 0 failures, 0 errors
其中—verbose選項會印出我們執行了哪些測試函式。
$ ruby test/tc_roman.rb --verbose
Loaded suite test/tc_roman
Started
test_range(RomanTest): .
test_to_s(RomanTest): .

Finished in 0.001581 seconds.

2 tests, 10 assertions, 0 failures, 0 errors

Automatically Running Unit Tests

當系統越來越大,寫的測試檔案越來越多,如果每次改個小Bug就要全部手動跑一次測試,那就非常的不符合Ruby愛好者的個性了(我聽說Ruby愛好者的個性都很懶惰),所以一定有方式讓我們可以只下一個command就把所有的測試都跑完的東西存在,那就是我下面要說的Rake/TestTask。
Rake的應用非常廣,包括migratioin/rdoc/capistrano...都有利用rake來進行自動執行,詳細的語法與使用方式可以參考http://rake.rubyforge.org/。
而自動執行測試,我們只需要在Project的根目錄中新增一個檔案,檔名為Rakefile,在檔案中加入:
  1. require 'rake/testtask'
  2. Rake::TestTask.new('test') do |t|
  3.     t.pattern='test/**/tc_*.rb'
  4.     t.warning=true
  5. end
不過這有一些前提:
1.測試檔案都放在test/目錄中,且檔案名稱都是tc_開頭,副檔名為rb。
2.要被測試的檔案都是放在lib/中。在我們開頭所include進來的'rake/testtask'中會把lib/加入到Ruby的Load Path中(請參考前一段Structure的部份)。
接下來你只要在project的根目錄中下命令rake test,你就會看到測試的結果了。我們也可以在Rakefile中加上一行:
rask :default => [:test]
那只要在project的根目錄中下命令rake就會幫我們自動執行,而不用下test這個參數了。

長大以後,我們都知道這個世界不是都按照預設在走的,測試檔案不一定都長這樣,要被測試的檔案不一定都放在lib/中。還好Rake::TestTask有幫我們想到這一點。例如說被測試的檔案是放在libs/中,而且測試檔案也需要include其他的測試檔案,那該怎麼辦呢?我們可以改寫成這樣:
  1. require 'rake/testtask'
  2. lib_dir=File.expand_path('libs')
  3. test_dir=File.expand_path('test')
  4. Rake::TestTask.new('test') do |t|
  5.     t.libs=[lib_dir, test_dir]
  6.     t.pattern='test/**/tc_*.rb'
  7.     t.warning=true
  8. end
Rake::TestRake有一個libs的屬性,會將libs的路徑加到Ruby的Load Path中,預設libs就是lib/。而在範例中,我們則加入了兩個(libs/與test/的絕對路徑)到Ruby的Load Path中。

一個大的應用程式中都會區分成很多個子系統,那我們可不可以單純只測試某些子系統呢?例如說我修改了IO的bug,我應該只需要跑IO相關檔案的測試,應該不需要連庫存系統也一起測試吧(不過根據經驗,就是因為這樣,所以常常出包)。那等到要Deploy時再做整個系統的測試就可以了。
還記得前面的Test Suite嗎,我們可以把相關的檔案作成Test Suite檔案,例如說IO相關的就作成ts_io.rb,庫存相關的就作成ts_inventory.rb,那Rakefile中就增加兩個task:
  1. Rake::TestTask.new('io-test') do |t|
  2.     t.test_file=['test/ts_io.rb']
  3.     t.warning=true
  4. end
  5. Rake::TestTask.new('inventory-test') do |t|
  6.     t.test_file=['test/ts_inventory.rb']
  7.     t.warning=true
  8. end
那要執行所有的Test Suite
  1. Rake::TestTask.new('all-test') do |t|
  2.     t.pattern='test/**/ts_*.rb'
  3.     t.warning=true
  4. end
那只做IO相關測試時,就執行rake io-test,要執行整個系統的測試時,就執行rake all-test。


« 十一月 2008
星期日星期一星期二星期三星期四星期五星期六
      
1
2
3
4
5
6
7
8
9
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
      
今日

Feeds

Search this blog

Links

Weblog menu

Today's referrers