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

Discussion in 'Ruby' started by James Wenton, May 23, 2010.

  1. James Wenton

    James Wenton Guest

    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?
     
    James Wenton, May 23, 2010
    #1
    1. Advertising

  2. On 5/23/10, James Wenton <> wrote:
    > 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.
     
    Caleb Clausen, May 24, 2010
    #2
    1. Advertising

  3. James Wenton

    Robert Dober Guest

    On Sun, May 23, 2010 at 4:00 PM, James Wenton <> wrote:
    > 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
     
    Robert Dober, May 24, 2010
    #3
  4. James Wenton

    Ryan Davis Guest

    On May 23, 2010, at 07:00 , James Wenton wrote:

    > 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.
     
    Ryan Davis, May 26, 2010
    #4
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Mauricio Fernandez
    Replies:
    1
    Views:
    162
    Peña, Botp
    Oct 16, 2006
  2. David Chelimsky
    Replies:
    0
    Views:
    120
    David Chelimsky
    Sep 15, 2009
  3. David Chelimsky
    Replies:
    0
    Views:
    122
    David Chelimsky
    Oct 3, 2010
  4. David Chelimsky
    Replies:
    0
    Views:
    114
    David Chelimsky
    Oct 10, 2010
  5. timr
    Replies:
    2
    Views:
    173
Loading...

Share This Page