以 Rspec 為例,創造一個自己的 DSL 工具吧!
什麼是 DSL? DSL 的存在意義是什麼?
DSL 為 Domain Specific Language 的縮寫,中文譯為「特定領域語言」。而 DSL 的開發通常是為了迎合某些特殊的需求,例如針對系統、平台、程式工具、軟體問題、商業邏輯等等領域,當人們發現現有的程式語言並沒有辦法好好地嵌合使用需求,DSL 就有其存在的價值。目前為人所知的 DSL 包括 CSS 以及 SQL,而這種易於人類閱讀的 DSL 也能夠讓程式設計師乃至於一般使用者都能享受 DSL 帶來的好處,甚至不懂程式設計的使用者都能夠針對自己的需求調整參數。
而 Ruby 本身由於程式設計面本身帶有易於閱讀的特質,因此非常多的 Gem 都是開發為 DSL 的形式,例如 AASM、whenever、以及最為廣為人知,最多人使用的測試框架 RSpec 等等。
這篇文章透過設計一個簡單的 RSpec 框架,來告訴大家一個 DSL 是如何開發而成的。
測試範例
我們會用下面的程式來舉例。
class CatFactory
def produce
"meow"
end
end
describe CatFactory
describe "#produce" do
it "produce a cat and meowing" do
expect(CatFactory.new.produce).to eq("meow")
end
it "does not produce a dog" do
expect(CatFactory.new.produce).not_to eq("woof")
end
end
end
這是一個相當簡單的範例,我們有個貓咪工廠,每當我們產出一隻貓就會喵一聲。下面的 Test Case 則有兩個,在 DSL 的形式之下,我們可以很明顯地讀懂這個測試想表達的意思:我們希望測試這個 #produce 函式可以產生出一隻貓,並且「喵」一聲,而不是產生出一隻狗。
實作 RSpec
接著我們會慢慢地實作出小型的測試框架,先從 describe 開始吧!
class Describe
def initialize(context, &block)
@context = context
instance_eval(&block)
end
# 為了讓我們可以使用 describe 來做成巢狀結構
def describe(context, &block)
Describe.new(context, &block)
end
end
由於我們希望 DSL 的形式是 describe CatFactory
而不是Describe.new(CatFactory)
,我們可以在 Describe 這個 class 外面新增一個 helper method:
# helper method
def describe(context, &block)
Describe.new(context, &block)
end
如此以來我們就可以利用 describe
來創造一個清楚明瞭的巢狀結構了。
接著我們希望把每個測試的 Case 都化為一個物件稱為 Example,因此我們要在創建一個新的 class。
class Example
def initialize(context, &block)
@context = context
instance_eval(&block)
end
def expect(result)
end
def to(expectation)
end
def not_to(expectation)
end
def eq(expectation)
end
end
讓我們來一個一個實作這些函式吧。
#expect 這個函式單純只是保存測試的結果,因此是最為容易的一個。
def expect(result)
@result = result
# 為了讓 #expect 之後可以繼續串 Example 裡頭的其他函數,所以我們這裡希望回傳 self。
self
end
#eq 這個函式很單純只是做出像 ==
一樣的運算,不過我們必須將它傳進 #to,並且和真正的結果對比。這裡我們會用 Proc 來處理。
def to(expectation)
@test_result = expectation.call(@result)
end
def eq(expectation)
Proc.new { |result| result.eql?(expectation) }
end
別忘了,我們還有一個 #not_to,不過 #not_to 其實就是 #to 的相反,因此就很容易了。
def not_to(expectation)
@test_result = !(expectation.call(@result))
end
顯示測試的結果
我們已經實作了大致的邏輯,現在我們希望可以在終端機中顯示我們的測試結果。這裡我們希望顯示出 describe 的巢狀結構,並且也列出所有 #it 表示的測試內容描述。為了表現出測試的結果是否成功與失敗,我們要運用 colorize
這個 Gem。
為了達到上述的功能,我們希望給 Example
一個 test_result
的 Reader,讓 Describe
可以得知測試的結果。
class Example
attr_reader :context, :test_result
# other code below
# ...
end
接著我們讓 Describe
可以用陣列儲存我們的 context 和 example:
class Describe
attr_reader :context, :examples, :describes
def initialize(context, &block)
@context = context
@describes = []
@examples = []
instance_eval &block
end
def describe(context, &block)
describes << Describe.new(context, &block)
end
def it(context, &block)
@examples << Example.new(context, &block)
end
end
最後,我們在 Describe
裡面實作一個 test
函式 來啟動整個測試:
def test
puts context
describes.each do |describe_node|
puts " " + describe_node.context
describe_node.examples.each do |example_node|
color = example_node.test_result ? :green : :red
puts " " + example_node.context.colorize(color)
end
end
end
在這個函式裡面我們可以看見,我們先印出最上位的 context,之後我們再印出每個巢狀結構下面的 describe
裡面的 context,最後則是印出 Example 的結果。而 color 的部分則是我們的 test_result 是 true 的情況下印出綠色,而 false 的情況下則印出紅色。
接著讓我賦予 test case 一個變數,然後啟動測試。
rspec = describe CatFactory
describe "#produce" do
it "produce a cat and meowing" do
expect(CatFactory.new.produce).to eq("meow")
end
it "does not produce a dog" do
expect(CatFactory.new.produce).not_to eq("woof")
end
end
end
rspec.test
當你執行這個 Ruby 檔案時,你應該會看到一個巢狀結構的顯示,代表你順利創造出了這個小型的 DSL 測試框架了!
CatFactory
#produce
produce cat and meowing
does not produce dog
總結
由於 Ruby 的種種特性(適合 Metaprogramming,簡潔的寫法,易讀的語句),讓 DSL 的實現變得更加容易,也使得很多的 Gem 成為非常洗鍊的 DSL 工具。未來在製作自己的工具的時候,不仿利用這些特性,也可以讓你的工具更加易於應用及推廣。