2010-05-23 75 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