开发者

How do I write an RSpec test to unit-test this interesting metaprogramming code?

开发者 https://www.devze.com 2022-12-31 23:02 出处:网络
Here\'s some simple code that, for each argument specified, will add specific get/set methods named after that argument. If you write attr_option :foo, :bar, then you will see #foo/foo= and #bar/bar=

Here's some simple code that, for each argument specified, will add specific get/set methods named after that argument. If you write attr_option :foo, :bar, then you will see #foo/foo= and #bar/bar= instance methods on 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

So far, so good. I want to write some RSpec tests to verify this code is actually doing what it's supposed to. But there's a problem! If I invoke attr_option :foo in one of the test methods, that method is now forever defined in Config. So a subsequent test will fail when it shouldn't, because foo is already defined:

  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 alrea开发者_Python百科dy defined
                                     # by a previous test.
    # ...
  end

Is there a way I can give each test an anonymous "clone" of the Config class which is independent of the others?


One very simple way to "clone" your Config class is to simply subclass it with an anonymous class:

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

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

This runs for me without error. This works because all instance variables and methods that get set are tied to the anonymous class instead of Configurator::Config.

The syntax Class.new Foo creates an anonymous class with Foo as a superclass.

Also, throwing an Exception in Ruby is incorrect; Exceptions are raised. throw is meant to be used like a goto, such as to break out of multiple nests. Read this Programming Ruby section for a good explanation on the differences.

As another style nitpick, try not to use if not ... in Ruby. That's what unless is for. But unless-else is poor style as well. I'd rewrite the inside of your args.each block as:

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
0

精彩评论

暂无评论...
验证码 换一张
取 消