How do I write an RSpec test to unit-test this interesting metaprogramming code?
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, throw
ing an Exception
in Ruby is incorrect; Exception
s are raise
d. 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
精彩评论