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

J

James Wenton

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?
 
C

Caleb Clausen

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?

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.
 
R

Robert Dober

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=3D` and `#bar/bar=3D` instance methods on `Config`:

=A0 =A0module Configurator
=A0 =A0 =A0class Config
=A0 =A0 =A0 =A0def initialize()
=A0 =A0 =A0 =A0 =A0@options =3D {}
=A0 =A0 =A0 =A0end

=A0 =A0 =A0 =A0def self.attr_option(*args)
=A0 =A0 =A0 =A0 =A0args.each do |a|
=A0 =A0 =A0 =A0 =A0 =A0if not self.method_defined?(a)
=A0 =A0 =A0 =A0 =A0 =A0 =A0define_method "#{a}" do
=A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0@options[:"#{a}"] ||=3D {}
=A0 =A0 =A0 =A0 =A0 =A0 =A0end

=A0 =A0 =A0 =A0 =A0 =A0 =A0define_method "#{a}=3D" do |v|
=A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0@options[:"#{a}"] =3D v
=A0 =A0 =A0 =A0 =A0 =A0 =A0end
=A0 =A0 =A0 =A0 =A0 =A0else
=A0 =A0 =A0 =A0 =A0 =A0 =A0throw Exception.new("already have attr_option = for #{a}")
=A0 =A0 =A0 =A0 =A0 =A0end
=A0 =A0 =A0 =A0 =A0end
=A0 =A0 =A0 =A0end
=A0 =A0 =A0end
=A0 =A0end

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:

=A0 =A0 =A0it "should support a specified option" do
=A0 =A0 =A0 =A0c =3D Configurator::Config
=A0 =A0 =A0 =A0c.attr_option :foo
=A0 =A0 =A0 =A0# ...
=A0 =A0 =A0end

=A0 =A0 =A0it "should support multiple options" do
=A0 =A0 =A0 =A0c =3D Configurator::Config
=A0 =A0 =A0 =A0c.attr_option :foo, :bar, :baz =A0 # Error! :foo already d= efined
=A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 = =A0 =A0 # by a previous test.
=A0 =A0 =A0 =A0# ...
=A0 =A0 =A0end
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 =3D Class::new RuntimeError

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

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



--=20
The best way to predict the future is to invent it.
-- Alan Kay
 
R

Ryan Davis

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:
=20
it "should support a specified option" do
c =3D Configurator::Config
c.attr_option :foo
# ...
end
=20
it "should support multiple options" do
c =3D 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 =3D 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.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,997
Messages
2,570,241
Members
46,831
Latest member
RusselWill

Latest Threads

Top