如何编写RSpec测试来对这个有趣的元编程代码进行单元测试?

时间:2010-05-23 13:49:46

标签: ruby metaprogramming rspec testing

这是一些简单的代码,对于指定的每个参数,将添加以该参数命名的特定get / set方法。如果您撰写attr_option :foo, :bar,那么您会在#foo/foo=上看到#bar/bar=Config个实例方法:

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类的匿名“克隆”,它独立于其他类?

1 个答案:

答案 0 :(得分: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作为超类。

另外,Ruby中的throw Exception是不正确的; Exceptionraise 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