聲明
本文系 sinatra 源碼系列第 2 篇。系列的目的是通過 sinatra 學習 ruby 編程技巧。文章按程序運行的先后順序挑重點分析,前一篇文章分析過的略去不說。水平很有限,所寫盡量給出可靠官方/討論鏈接,不坑路人。
重要提醒
一定要先安裝 1.8 版本的 ruby ,因為 1.9+ 的 ruby ,String 的實例是不響應 each 方法的,這會直接導致 rack 報錯。可以使用 rvm 安裝 1.8.7 版本的 ruby ,如果使用 rvm ,請先升級到最新版本,否則安裝 1.8.7 的 ruby 時也會報錯。
列一下本人運行 sinatra 0.1.0 用到的 ruby 和關鍵 gem 的版本:
- ruby-1.8.7-p374
- rack 1.4.1
- mongrel 1.1.5
change log
- 支持設置運行環境
- 支持 session
- 支持在路由的資源路徑中傳入變量
- 增加測試用例
- 支持直接輸出靜態資源
- 支持渲染 layout
- 增加處理請求完成后的事件回調
- 支持后臺日志實時打印
loader.rb
sinatra 用 Loader 模塊來加載/重新加載文件。用到 Set ,無需擔心重復加載相同的文件。把 load_file
重命名為 load_files
,也拯救了有強迫癥的程序員。
sinatra 接下來將會這樣使用 Loader :
Sinatra::Loader.load_files Dir.glob(SINATRA_ROOT + '/lib/sinatra/core_ext/*.rb')
要注意如果 core_ext 目錄下有多個文件, Dir.glob 是不保證按一定順序(比如字母順序)加載文件的,討論見此, 1.8.7 版本的 ruby ,其 Set 也不保證 each 的順序一致,來源見此。
kernel.rb
這里擴展了一個很有意思的方法 silence_warnings ,如你所見,就是屏蔽警告用的,用法如下:
silence_warnings do
value = noisy_call # no warning voiced
end
sinatra 只想在調用 silence_warnings 時屏蔽警告,其他時候顯示警告。有時候我們也有類似的需求:調用某個方法之前改變某個配置,調用完了再把配置改回去。這涉及到保存配置和處理異常,可以借鑒 sinatra 在此處的做法。
繼承關系及至對象模型
sinatra 在 core_ext 目錄下,先后擴展了 Class, Module, Kernel, Object, Hash 等多個類,它們之間是什么關系呢?這個問題又牽扯到另一個終極問題: ruby 的對象模型是什么?當你清楚 ruby 的對象模型后,眾多類之間的關系就不在話下了。
空說無益,先教大家幾個探索對象模型的方法,打開 irb ,寫兩個簡單的類:
class A; end
class B < A; end
我們知道 B 繼承自 A ,B 有一個方法可以顯示自己的父類是誰:
B.superclass #=> A
# 當 B 繼承了 A ,我們就說 A 是 B 的超類(這是 ruby 的中文術語吧,一般都叫父類的)
我們從 B 實例化出一個對象 b ,b 也有方法可以打印自己是屬于哪個類的實例:
b = B.new
b.class #=> B
我們要知道 ruby 中,類也是實例,如果在類上面調用 class 方法會打印什么呢?
A.class #=> Class
B.class #=> Class
Hash.class #=> Class
Class.class #=> Class
Module.class #=> Class
就連在 Class 上調用 class 方法也得到 Class 。這里得出一個結論,所有類都是 Class 類的實例。
我們再回到繼承這個話題,除了有辦法看到一個類的超類,還有辦法看到一個類的祖先鏈:
B.ancestors #=> [B, A, Object, Kernel, BasicObject]
#=> 上面的結果是在 ruby 2.0.0 版本中得到的,你的版本可能有少許不同
可以看到 B 繼承自 A ,A 繼承自 Object , Object 繼承自 Kernel , Kernel 繼承自 BasicObject 。嗯,這種說法是不對的,實際上 Object 繼承自 BasicObject , Kernel 模塊是被 Object include 進來的:
class Object < BasicObject
include Kernel
end
被 include 進來的模塊,都是剛好插入到類的祖先鏈的超類位置。
你會發現,幾乎所有的類的祖先鏈都包含 Object, Kernel, BasicObject 這三個類:
A.ancestors #=> [A, Object, Kernel, BasicObject]
Array.ancestors #=> [Array, Enumerable, Object, Kernel, BasicObject]
Fixnum.ancestors #=> [Fixnum, Integer, Numeric, Comparable, Object, Kernel, BasicObject]
String.ancestors #=> [String, Comparable, Object, Kernel, BasicObject]
這三個類可是繼承鏈的發源地啊。
你會發覺我們還沒有講到 Module , Module 是 Class 的超類:
Class.ancestors #=> [Class, Module, Object, Kernel, BasicObject]
以上是基礎版的 ruby 對象模型,其實也沒說多少。
metaid.rb
sinatra 在這個里做了一個相當頂層的——Object——擴展,要理解這樣做的目的,首先要明白 ruby 是怎樣尋找一個方法的。打開 irb ,輸入:
class A
def method_1
puts 'I am instance method'
end
end
首先要知道:方法都是存放在 類 ,而不是類的實例中的。如果類實例調用了某個方法,而在實例的類中找不到該方法,那么會沿著祖先鏈一直往上面找。如果最終還是找不到,就會轉而調用該類的 method_missing 方法,如果該類沒定義 method_missing 方法,也會沿祖先鏈一直往上找,直到 BasicObject(2.3.1 版 ruby ,1.8.7 版 ruby 是在 Kernel 上定義) 的 method_missing 方法。拿上面的例子來說:
a = A.new
a.method_1 #=> 'I am instance method'
class A
def method_missing(method_name, *args, &block)
puts "you have called method #{method_name}"
end
end
a.method_2 #=> 'you have called method method_2'
class B < A; end
b = B.new
b.method_1 #=> 'I am instance method'
b.method_3 #=> 'you have called method method_3'
有時候我們還會定義這樣的方法:
class A
def self.method_4
puts 'I am singleton method method_4'
end
class << self
def method_5
puts 'I am singleton method method_5'
end
end
def A.method_6
puts 'I am singleton method method_6'
end
end
以上三種方法不同之處只在于名字不同,它們都是類的單例方法(singleton method)。剛說過:“方法都是存放在 類 ,而不是類的實例中的”,單例方法也是如此,它存在于實例的 metaclass 中(或者叫做 eigenclass ,官方稱作 singleton class)。metaclass 一直待在我們的視野范圍之外,官方沒有提供讓它們現形的方法, sinatra 要做的就是擴展一套這樣的方法。
metaid.rb 第 6、7 行的寫法很帥氣,但也很難看懂,稍為整理一下:
def metaclass
#1
class << self #2
#3
self
end
end
def meta_eval &blk
metaclass.instance_eval &blk
end
分析 metaclass 方法,在 #1 處,如果把 self 打印出來,這個 self 會是 Object 的實例(具體得看是誰調用 metaclass 方法);在 #2 處,運用 ruby 提供的語法 class << self
,把 class << self; self; end
塊中的 self 設置為 Object 實例的 metaclass ;所以在 #3 處,如果把 self 打印出來,這個 self 會是 Object 實例的 metaclass ,而這個 self 會作為塊的結果返回, metaclass 方法又將塊的結果返回,最終得到 Object 實例的 metaclass 。
附上對 metaclass 的分析參考資料:
- Metaprogramming Ruby 2: Program Like the Ruby Pros (Facets of Ruby)
- seeingMetaclassesClearly
- Metaprogramming in Ruby: It's All About the Self
symbol.rb
在系列第 1 篇文章說過,可以給 Symbol 定義一個 to_proc 方法,方便與 & 操作符配合使用。 sinatra 定義了一個看上去不一樣的 to_proc :
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
但做的事情跟第 1 篇文章中的一樣。
*
(splat operator)出現了兩次,意義剛好相反,第一次出現是把調用方法時傳進來的參數變為一個數組,第二次出現是把一個數組拆散成一個個的參數傳到方法中。在 1.8 版本的 ruby ,只要是能響應 to_ary 方法的對象都可以這樣用:
class Foo
def to_ary
[1,2,3]
end
end
a, *b = Foo.new #=> a = 1, b = [2,3]
def some_method(p1,p2,p3)
p "#{p1} #{p2} #{p3}"
end
some_method(*Foo.new) #=> 1 2 3
上面這個例子出自此處。
args.shift
會刪除并返回 args 數組第一個元素。
__send__
方法跟 send
方法做的事情一樣。因為 send 這個單詞太普通、常用了,很容易被程序員覆寫,所以 ruby 又另外提供一個 __send__
,如果不小心覆寫這個方法, ruby 會提示警告:
warning: redefining `__send__' may cause serious problem
附上對這個方法討論的鏈接
module.rb
module.rb 在 Module 擴展了一個 attr_with_default 方法,這個方法類似 Class 中的 cattr_accessor ,只不過多了個默認值。
這里出現元編程中常見的 define_method
方法,它是定義在 Module 中的私有方法,用來動態地生成方法。完整文檔可以看這里。
一般情況下 define_method
只能在定義類時直接調用(此時 self 指向類本身),如:
class A
define_method(:m_a) { p 'm_a' }
end
A.new.m_a #=> m_a
如果要在實例方法里調用 define_method
,這樣寫會出報找不到方法錯誤:
class B
def create_mehtod(sym, &block)
define_method(sym, &block)
end
end
B.new.create_method(:m_b) {p 'm_b'} #=> NoMethodError: undefined method `define_method'
回顧 ruby 尋找方法的步驟:先到實例的類中找,找不到就沿著類的祖先鏈找,打印 B 的祖先鏈,里面并沒有 Module ,這就是出錯的原因:
B.ancestors #=> [B, Object, Kernel]
而在定義類時直接調用 define_method
不報錯,是因為此時 self 指向 A ,而 A 作為實例的話,它的類是 Class ,打印 Class 的祖先鏈,里面就有 Module:
Class.ancestors #=> [Class, Module, Object, Kernel]
在調用 define_method
時把 self 指向 B ,還是會報錯:
class B
def create_mehtod(sym, &block)
self.class.define_method(sym, &block)
end
end
B.new.create_method(:m_b) {p 'm_b'} #=> NoMethodError: private method `define_method' called for B:Class
因為 define_method
是私有方法,不能顯式調用,官方文檔給出了解決辦法:
class B
def create_mehtod(sym, &block)
self.class.send(:define_method, sym, &block)
end
end
b = B.new.create_method(:m_b) {p 'm_b'}
b.m_b #=> m_b
request.rb
這里重新打開了 Rack::Request ,擴展了 request_method 方法。這樣做的緣由是:html 的 form 元素只支持 GET 和 POST 方法, RESTful 定義的方法至少有 GET/POST/PUT/DELETE 四種,為了讓 form 也用上 PUT 和 DELETE 方法, sinatra 檢測 POST 請求中的 _method 參數,如果是 PUT 或者 DELETE ,就直接替換 POST 。相關討論見此
environment.rb
在加載完 core_ext 和 rack_ext 目錄下的文件后,會加載 sinatra 目錄下的文件,一時不知從何下手分析,看到后面有行代碼:
Sinatra::Environment.prepare
就從 environment.rb 說起吧。
** ARGV **
Environment 的 prepare 方法用來解釋參數。 ARGV
是定義在 Object 中的常量,并且是 Array 的實例,表示在命令行運行腳本文件時傳入的參數列表。
options.rb
parse! 實際上沒有用到傳進來的參數,它用的還是 ARGV 。
這個版本的 sinatra 開始區分運行腳本的環境(test/development/production),如果當前處在 test 環境, parse! 方法立即返回。
接下來解釋參數的任務就交給 OptionParser 了。
這里有一句 env.intern
。 env 是一個 String 實例, intern 方法獲取字符串在 ruby 的內部實現(internal representation)。 ruby 最終會把字符串轉換為符號,所以這個方法跟 to_sym 方法做一樣的事情。 參見相關討論 (PS. 討論中提及為什么 ruby 給同一個方法取不同的名字,很有啟發意義)
logger.rb
與前一個版本相比,這個文件多了一行代碼:
define_method n do |message|
@stream.puts message
@stream.flush #多了這一行
end
@stream 是一個 IO 實例, flush 方法將 IO 實例中緩存的數據寫到操作系統中去(官方文檔中解釋操作系統仍然有可能緩存起來,所以并沒有保證寫到設備/文件中)。舉個例子,在早期的 ruby 中,下面這段代碼會等待 10 秒,然后在同一行打印 5 個點:
5.times do
print '.'
sleep 2
end
要想每 2 秒打印一個點,可以在 print '.'
下面加上一句 $stdout.flush
。
緩存輸出,直到打印換行符或者緩存滿了,這個特性來源于 c 言語標準庫,初衷應該是減少系統調用。后來不知道是 c 言語標準庫還是 ruby 作了改動,修復了上面那個問題。
推薦幾篇有關 Ruby IO 的文章:
irb.rb
在運行 sinatra 時加上 -c 參數,就會用 console 模式啟動 sinatra 。
這個文件只定義了 start! 方法。在 ruby 中定義末尾帶感嘆號(!)的方法,意味著這個方法比不帶感嘆號的危險,要小心使用。
start! 方法首先讓 Object 加載 TestMethods 模塊, include
方法是 Object 的私有方法,所以要使用 Object.send 加載(還記得這個技巧在 module.rb 那一節說過嗎)。
接著給 Object 類擴展了 reload! 和 show! 兩個方法(建議現在就運行 sinatra 的 console 模式,動手玩玩這兩個方法)。
show! 調用了 IO.popen 方法。如果你想開一個子進程來調用外部命令,而且還想把外部命令的標準輸入和標準輸出跟 ruby 連接起來,那這個方法能滿足你的需求。 popen 里的 p 指代 pipeline (管道)。管道是進程間通信的一種方式。
舉個使用 popen 的例子:
IO.popen('tail -3', 'w+') do |pipe|
# ruby 會開一個子進程來運行這個 block
# 管道中屬于 ruby 的這一頭會作為參數傳進來
1.upto(100) do { |i| pipe.puts "line #{i}" }
pipe.close_write #在讀取流之前一定要先把寫入關閉,否則讀取會阻塞
puts pipe.read
end
# line 98
# line 99
# line 100
show! 方法的意圖是打開文本編輯器,并寫入 TestMethods 模塊中的幾個方法 status / headers / body 的返回結果。
舉個例子,假設你能在命令行使用 subl
命令打開 sublime text 。你可以先跳轉到 examples/hello 目錄下,輸入:
EDITOR=subl ruby hello.rb -c
這時你會進入 irb ,然后輸入:
show!
這時你的 sublime text 就會被打開,里面已經寫入了一些內容:
<!--
# Status: 404
# Headers: {"Content-Type"=>"text/html", "Content-Length"=>"0"}
-->
推薦一本用 ruby 來描述的關于進程的入門書 理解Unix進程,里面有提及進程間通信的方式。
還有幾個關于 popen 的文檔/討論
接下來 sinatra 先清空 ARGV 。如果當前目錄(啟動 sinatra 時所在的目錄,而不是當前文件所在的目錄, 運行 Dir.pwd
可以看到)下有 '.irbrc' 文件,就把它保存到環境變量中, irb 會在啟動時加載這個文件。
當用戶退出 irb 時,立即運行 exit!
,這樣就退出了 sinatra 。
exit!
和 exit
的區別是前者會跳過退出時的處理程序(比如 at_exit ),前者默認的退出狀態是 false ,而后者默認的退出狀態是 true ( ruby 不同版本有不同的退出返回值, 1.8.7 版本 exit
默認返回 0 , exit!
默認返回 -1 。 unix 會把返回值 0 當成 true ,其它返回值當成 false )。
server.rb
Server#start 方法首先調用 Server#tail 方法打印 log file 里面的內容。 tail 方法另開一個線程打開 log file ,然后不斷地檢查( 1 秒 1 次)它有沒有被改動,如果有則打印自上一次文件流的位置到最新文件流的末尾之間的內容。這段代碼可以再精簡一點:
File.open(log_file, 'r') do |f|
loop do
if f.mtime > last_checked
last_checked = f.mtime
puts f.read
end
end
end
IO#read 方法會把 cursor 的位置定位到流的末尾,所以不需要手動調用 IO#seek 重新定位 cursor 的位置,這一點可以在調用 IO#read 之后再 打印 IO#pos 的結果證明。
Server#start 最后調用 Thread#kill 方法殺掉這個線程。這一步很有可能是多余的,因為如果當前線程( main thread )結束了,所有其他線程都將會被殺死。
sinatra 用到多進程和多線程,兩者的區別以及使用時機可參考這篇文章和這篇文章
stackoverflow 的一些討論:
dispatcher.rb
在開發環境(development)中,sinatra 響應每一個請求前都會重新加載依賴文件以及在命令行中被 ruby 直接執行的腳本文件:
Loader.reload! if Options.environment == :development
這樣在開發環境中改動文件不需要重啟就生效。 Loader.reload!
方法會重新加載被執行的腳本文件,看上去會產生循環加載的問題,舉個例子,跳轉到 examples/hello/ 目錄下,在命令行中輸入:
ruby hello.rb -c
# => 通過 require 'sinatra' , 加載 /lib/sinatra 目錄下的相關文件,也把這些文件加載到 loaded_files 中
此時在命令行中輸入:
reload!
# => 重新加載 loaded_files 中的文件,然后加載 hello.rb 文件
hello.rb 文件中有 require 'sinatra'
,這會不會導致 ruby 重新加載 sinatra 呢?
不會。
Kernel#require
方法會在 $LOAD_PATH
中查找要加載的文件,它也會幫你加上 .rb 或者 .so 文件后綴。比如此處的 require 'sinatra'
,它會在 lib/ 目錄下找到 sinatra.rb 文件。
已經被 Kernel#require
加載過的文件會保存在 $"
變量中,Kernel#require
不會再次加載已經加載過的文件。
Kernel#load
方法要求在使用時寫上文件路徑以及文件后綴,如果文件路徑不是絕對路徑,會在 $LOAD_PATH
中查找文件。
Kernel#load
會再次加載已經加載過的文件。
想關討論可參考How does load differ from require in Ruby?
ruby 預先定義了不少變量、常量,這是列表
sessions.rb
Rack::Session::Cookie 實現了基于 cookie 的 session 管理功能,只要瀏覽器發過來的 cookie 中有 key 為 session_id 的鍵值對,Rack 就能借此保存、讀取數據。
Rack::Session::Cookie 最初并沒有實現基于 session_id 讀寫數據,所有數據都保存在 env['rack.session'] 下面,源碼見此。 0.1.0 的 sinatra 應該就是使用這個最初的實現,通過控制臺可以看到 cookie 中直接使用 rack.session 保存加密后的數據。
cookie 功能默認開啟,如果要關閉它,可以在加載之后調用 dsl.rb 中定義的 sessions
方法:
sessions :off
sinatra 還提供 session
方法返回已保存的 session ,方便使用 cookie 功能,下面是一個例子:
#!usr/bin/env ruby
#file examples/you_say.rb
require 'sinatra'
get '/' do
session[:you_say] = params[:you_say] || 'no'
# 注意 session 和 params 都要用 symbol 作 key
'hello'
end
get '/session' do
session[:you_say]
end
先訪問 localhost:4567/?you_say=hi
,再訪問 localhost:4567/session
,能看到頁面顯示 'hi' 。
event.rb
** EventManager ** 負責注冊事件、匹配事件。
它調用 determine_event
匹配路由、方法,如果匹配不到,就調用 present_error
去找用戶自定義的 404 路由處理器,如果用戶沒有預先定義,就調用 not_found
,使用默認的 404 處理器。
Object#method 根據名字返回方法(或者拋出 NameError 異常),被返回方法的 receiver 就是調用 Object#method 的對象,而且被返回方法就像閉包一樣,能訪問此對象的實例變量以及方法。舉例如下:
class A
def initialize(v)
@k = v
end
def get_put_k_method
method(:put_k)
end
def put_k
puts "k value is #{@k}"
end
def get_another
method(:set_put_k)
end
def set_put_k(new_k=nil)
@k = new_k
put_k
end
end
a = A.new('hi')
m = a.get_put_k_method
m.call #=> k value is hi
m2 = a.get_another
m2.call('hello') #=> k value is hello
Event 類把路由匹配交由 Route 處理,還增加了事件處理回調 after_filters 。
StaticEvent 負責處理靜態資源,用法跟其他路由一樣:
get '/', 'home'
static '/p', 'public'
#請求 '/p/css/bootstrap.css' 會被映射到 'public/css/bootstrap.css'
StaticEvent 的 attend
方法中有這樣一行: context.body self
,之后還定義了 each
方法。這樣做全因為 Rack 要求 http body 對象響應 each 方法。
each
方法用二進制讀取模式打開靜態文件。 IO#read 接受字節長度作為參數,從流中讀取指定長度的字節,如果一開始就讀到 EOF ,會返回 nil 。
8192 字節(8KB)是常用的 chunk size 。
在設置響應頭的 Content-Type 時,用到了#[]
方法:
File.extname(@filename)[1..-1]
# '.rb'[1..-1] => 'rb'
此處傳入的 Range 參數((1..-1)),表示的范圍是:從左邊數起第 2 個元素到右邊數起第 1 個元素。
renderer.rb
EventContext 加載了 Sinatra::Renderer 模塊,此模塊為其他渲染方法提供基礎方法,比如 Sinatra::Erb 和 Sinatra::Haml ,你還可以定制自己的渲染方法。注釋里寫了一個定制的例子,如果還有不清楚的地方,可以查看對應的測試用例: renderer_test.rb 。
render
方法會根據參數 renderer ,動態調用真正實現渲染的方法 result_method 。
render
方法把傳進來的 block 當作 layout 的來源之一。如果請求有對應的 layout ,在第二次調用 result_method 方法時把 layout 當成是 template 參數傳進去。
route.rb
在實例化每個 Event 時,會一并實例化一個 Route 。而每一次調用 Event#attend
,會先把 @route.params
合并到 request.params
中。這就把用戶具體的請求路徑與路由的 symbol 對應起來。如:
get '/:controller/:method' do
"you #{params[:controller]} #{params[:method]}"
end
# 當用戶請求 '/say/hi' 時
# 會返回 "you say hi"
Route#extract_keys
把路由中的 symbol 提取出來,如:
temp_arr = "/:some/:words".scan(/:\w+/)
#=> temp_arr = [":some",":words"]
temp_arr.map { |raw| eval(raw) } #=> [:some, :words]
Route#genereate_route
生成用于匹配用戶請求的路由。路由又分兩種,帶格式(format)和不帶格式的,默認格式是 html 。
Route#to_regex_route
把路由轉換成正則表達式,在點(.)前面加上反斜杠,把 symbol
替換成 '([^\/.,;?]+)'
。在匹配成功后可以用 captures
方法找到用戶請求的路徑。如:
class A
def to_regex_route(template)
/^#{template.gsub(/\./,'\.').gsub(/:\w+/,'([^\/.,;?]+)')}$/
end
end
a = A.new
reg = a.to_regex_route('/:path/:to/:file.html')
# reg => (?-mix:^\/([^\/.+,;?])\/([^\/.+,;?])\/([^\/.+,;?])\.html$)
'/a/b/c.html'.match(reg).captures
# => ['a','b','c']
/([^\/.,;?]+)/
匹配不是斜杠(/),點(.),逗號(,),分號(;),問號(?)的其他字符。
Route#recognize
會在 Event#attend
中調用,所以每次都得先清空 @params
。
如果成功匹配用戶請求的路徑,接下來就把 symbol 和具體的路徑組合起來:
@keys.zip(param_values).to_hash
Array#zip
方法用法舉例:
[1,2,3].zip([4,5,6]) #=> [[1,4],[2,5],[3,6]]
Array#to_hash
方法是 sinatra 擴展的。
一些方法參考:
dsl.rb
dsl.rb 文件的最后調用 include Sinatra::Dsl
把 Sinatra::Dsl 模塊放到 main 對象祖先鏈的父節點位置,這樣就可以把 Sinatra::Dsl 定義的方法當作實例方法調用。
也可以把 include Sinatra::Dsl
替換成 extend Sinatra::Dsl
,后者把 Sinatra::Dsl 定義的方法當作單例方法調用。
看出問題了嗎?
main 對象同時作為 Object class 的實例以及 Object class 本身去調用方法,否則不能解釋它既可以調用實例方法又可以調用單例方法。
有一篇文章展示了神奇 main 對象。
test
這一版本補充了單元測試。跑測試用例之前要先安裝兩個 gem : mocha(0.5.6), test-spec(0.10.0) 。
還要在 test/helper.rb 文件中,加載 mocha 和 test/sepc 時把 stringio
也加載進來,否則 request_test.rb 會跑不過。
helper.rb 里把 Sinatra::TestMethods
include
到 Test::Unit::TestCase
中,因而每個測試都可以使用 Sinatra::TestMethods 提供的方法。
Rack::MockRequest
讓 Sinatra::TestMethods
模塊里的幾個方法不需要產生真實的 http 請求,就能調用到 sinatra 定義的請求處理器。詳見 MockRequest 的文檔。
要跑所有測試用例,可以在根目錄下運行:
find ./test/sinatra -name '*.rb' | xargs -n1 ruby
全文完。