2010-05-23 8 views
2

Voici un code simple qui, pour chaque argument spécifié, ajoutera des méthodes get/set spécifiques nommées après cet argument. Si vous écrivez attr_option :foo, :bar, vous verrez #foo/foo= et #bar/bar= méthodes d'instance sur Config:Comment écrire un test RSpec pour tester ce code de métaprogrammation intéressant?

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 

Jusqu'à présent, si bon. Je veux écrire des tests RSpec pour vérifier que ce code est en train de faire ce qu'il est censé faire. Mais il y a un problème! Si j'appelle attr_option :foo dans l'une des méthodes de test, cette méthode est maintenant définie pour toujours dans Config. Donc, un test ultérieur échouera quand il ne devrait pas, parce que foo est déjà défini:

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 

Est-il possible que je peux donner à chaque essai, un « clone » anonyme de la classe Config qui est indépendant des autres?

Répondre

5

Une façon très simple « clone » votre classe Config est de sous-classe simplement avec une classe anonyme:

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

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

Cela va pour moi sans erreur. Cela fonctionne car toutes les variables d'instance et méthodes qui sont définies sont liées à la classe anonyme au lieu de Configurator::Config. La syntaxe Class.new Foo crée une classe anonyme avec Foo en tant que superclasse.

En outre, throw un inG Exception en Ruby est incorrect; Exception s sont raise d. throw est destiné à être utilisé comme un goto, de sorte à sortir de plusieurs nids. Lire this Programming Ruby section pour une bonne explication sur les différences.

Comme un autre style nitpick, essayez de ne pas utiliser if not ... dans Ruby. C'est pour cela que unless est pour. Mais à moins que ce soit un style pauvre aussi. Je réécrirais l'intérieur de votre bloc args.each en tant que:

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 
+1

+1 pour des conseils solides et une approche novatrice; Je n'aurais pas pensé créer des classes anonymes et je pensais à créer un module anonyme avec la classe et à le détruire ensuite. C'est clairement une meilleure solution. –