Как мне написать тест RSpec для модульного тестирования этого интересного кода метапрограммирования? - PullRequest
2 голосов
/ 23 мая 2010

Вот простой код, который для каждого указанного аргумента будет добавлять определенные методы get / set, названные в честь этого аргумента. Если вы напишите attr_option :foo, :bar, то вы увидите #foo/foo= и #bar/bar= методы экземпляра на 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

Пока все хорошо. Я хочу написать несколько тестов RSpec, чтобы убедиться, что этот код действительно выполняет то, что должен. Но есть проблема! Если я вызову attr_option :foo в одном из тестовых методов, этот метод теперь навсегда определен в Config. Таким образом, последующий тест завершится неудачей, когда он не должен, потому что foo уже определено:

  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

Есть ли способ, которым я могу дать каждому тесту анонимный "клон" класса Config, который не зависит от других?

1 Ответ

5 голосов
/ 23 мая 2010

Один очень простой способ «клонировать» ваш класс Config - просто создать его подкласс с анонимным классом:

c = Class.new Configurator::Config
c.attr_option :foo

d = Class.new Configurator::Config
d.attr_option :foo, :bar

Это работает для меня без ошибок. Это работает, потому что все переменные экземпляра и устанавливаемые методы привязаны к анонимному классу вместо Configurator::Config.

Синтаксис Class.new Foo создает анонимный класс с Foo в качестве суперкласса.

Кроме того, throw в Exception в Ruby неверен; Exception s raise d. throw предназначен для использования как goto, например, для разрыва нескольких гнезд. Прочитайте этот раздел Ruby по программированию для хорошего объяснения различий.

В качестве еще одного стиля придираться, старайтесь не использовать if not ... в Ruby. Вот для чего unless. Но если только не плохой стиль. Я бы переписал внутреннюю часть вашего args.each блока как:

raise "already have attr_option for #{a}" if self.method_defined?(a)
define_method "#{a}" do
  @options[:"#{a}"] ||= {}
end

define_method "#{a}=" do |v|
  @options[:"#{a}"] = v
end
...