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

I’m a little stumped by this problem. 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 already 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?

On 5/23/10, James W. [email protected] wrote:

        else

now forever defined in Config. So a subsequent test will fail when it
c.attr_option :foo, :bar, :baz # Error! :foo already 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?

As I see it, you have several options:

  1. remove the exception you are raising at the end of attr_option.
  2. intercept and ignore that exception when you call attr_option in your
    tests.
  3. remove the methods you added at the end of each test. Something
    like this should work:
    c.send :undef_method, :foo
  4. (what you were asking about) make a copy of Configurator::Config
    before changing it. This should work:
    c=Configurator::Config.clone

PS: c is an especially unlucky choice for a local variable name, I
have found. If you ever have to run your program under one of the
console-mode debuggers (I do this all the time) it will get confused
with the continue command, which is abbreviated c, often with highly
frustrating results.

On Sun, May 23, 2010 at 4:00 PM, James W. [email protected] wrote:

       else

now forever defined in Config. So a subsequent test will fail when it
c.attr_option :foo, :bar, :baz # Error! :foo already defined
# by a previous test.
# …
end
Caleb is right, c is not a good name ;), however

lambda{ c.attr_option… }.should raise_error( WhatWasIt)

I call it WhatWasIt because you really should define your own Exception,
e.g.
IllegalMonitorState = Class::new RuntimeError

please take care to subclass RuntimeError, subclassing Exception is
waaaay toooo general.

HTH
R.

On May 23, 2010, at 07:00 , James W. wrote:

 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

Caleb and Robert are nit-picking… ‘c’ is a perfectly fine name for a
variable in a 4 line test/spec.

The problem you’re having is easily solved by using anonymous
subclasses:

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

That makes a throwaway class that has all the same features of the
superclass without any of the infectious properties of calling your attr
methods on the real thing.