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

    def self.attr_option(*args)
    args.each do |a|
    if not self.method_defined?(a)
    define_method "#{a}" do
    @options[:"#{a}"] ||= {}

    define_method "#{a}=" do |v|
    @options[:"#{a}"] = v
    throw"already have attr_option for #{a}")

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

    it "should support multiple options" do
    c = Configurator::Config
    c.attr_option :foo, :bar, :baz # Error! :foo already defined
    # by a previous test.
    # ...

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

    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
    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,
    IllegalMonitorState =3D Class::new RuntimeError

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


    The best way to predict the future is to invent it.
    -- Alan Kay
    Robert Dober, May 24, 2010
  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 =

    it "should support a specified option" do
    c =3D
    c.attr_option :foo
    # ...

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