2010-05-23 80 views
2

下面是一些簡單的代碼,對於指定的每個參數,都會添加以該參數命名的特定get/set方法。如果你寫attr_option :foo, :bar,然後你會看到Config#foo/foo=#bar/bar=實例方法:如何編寫RSpec測試來單元測試這個有趣的元編程代碼?

module Configurator 
    class Config 
    def initialize() 
     @options = {} 
    end 

    def self.attr_option(*args) 
     args.each do |a| 
     if not self.method_defined?(a) 
      define_method "#{a}" do 
      @options[:"#{a}"] ||= {} 
      end 

      define_method "#{a}=" do |v| 
      @options[:"#{a}"] = v 
      end 
     else 
      throw Exception.new("already have attr_option for #{a}") 
     end 
     end 
    end 
    end 
end 

到目前爲止,一切都很好。我想寫一些RSpec測試來驗證這段代碼實際上正在做它應該做的事情。但是有一個問題!如果我在其中一種測試方法中調用attr_option :foo,則現在在Config中永遠定義該方法。所以,因爲foo已經定義的後續測試將當它不應該失敗:

it "should support a specified option" do 
    c = Configurator::Config 
    c.attr_option :foo 
    # ... 
    end 

    it "should support multiple options" do 
    c = Configurator::Config 
    c.attr_option :foo, :bar, :baz # Error! :foo already defined 
            # by a previous test. 
    # ... 
    end 

有沒有一種方法,我可以給每個測試Config類的匿名「克隆」,這是獨立於其他的?

回答

5

一個非常簡單的方法來「克隆」你Config類是簡單地使用匿名類繼承它:

c = Class.new Configurator::Config 
c.attr_option :foo 

d = Class.new Configurator::Config 
d.attr_option :foo, :bar 

這將運行對我來說沒有錯誤。這是可行的,因爲所有設置的實例變量和方法都與匿名類綁定,而不是Configurator::Config

語法Class.new Foo創建一個匿名類,其中Foo作爲超類。

另外,throw在Ruby中的Exception不正確; Exception s是raise d。 throw意在像goto一樣使用,例如突破多個巢。閱讀this Programming Ruby section可以很好地解釋差異。

作爲另一種風格挑剔,儘量不要在Ruby中使用if not ...。這就是unless的用途。但除非 - 否則也是不好的風格。我會將args.each區塊的內部改寫爲:

raise "already have attr_option for #{a}" if self.method_defined?(a) 
define_method "#{a}" do 
    @options[:"#{a}"] ||= {} 
end 

define_method "#{a}=" do |v| 
    @options[:"#{a}"] = v 
end 
+1

+1對於堅實的建議和新穎的方法;我不會想到創建匿名類,並且正在考慮像創建類的匿名模塊然後銷燬它。這顯然是更好的解決方案。 – 2010-05-23 15:37:25