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

  2. 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. Advertisements

  3. James Wenton

    Robert Dober Guest

    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.


    --=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

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

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 (here). After that, you can post your question and our members will help you out.