2012-08-26 22 views
18

¿Cómo reinicio un objeto singleton en Ruby? Sé que uno nunca querría hacer esto en código real, pero ¿qué pasa con las pruebas unitarias?Restablecer una instancia de singleton en Ruby

Aquí es lo que estoy tratando de hacer en una prueba RSpec -

describe MySingleton, "#not_initialised" do 
    it "raises an exception" do 
    expect {MySingleton.get_something}.to raise_error(RuntimeError) 
    end 
end 

Se produce un error porque una de mis pruebas anteriores inicializa el objeto singleton. He intentado seguir el consejo de Ian White del enlace this, que esencialmente mono actualiza a Singleton para proporcionar un método reset_instance, pero obtengo una excepción de método 'reset_instance' sin definir.

require 'singleton' 

class <<Singleton 
    def included_with_reset(klass) 
    included_without_reset(klass) 
    class <<klass 
     def reset_instance 
     Singleton.send :__init__, self 
     self 
     end 
    end 
    end 
    alias_method :included_without_reset, :included 
    alias_method :included, :included_with_reset 
end 

describe MySingleton, "#not_initialised" do 
    it "raises an exception" do 
    MySingleton.reset_instance 
    expect {MySingleton.get_something}.to raise_error(RuntimeError) 
    end 
end 

¿Cuál es la forma más idiomática de hacer esto en Ruby?

Respuesta

25

Pregunta difícil, los singletons son ásperos. En parte por la razón por la que está mostrando (cómo restablecerlo), y en parte porque hacen suposiciones que tienden a morderlo más tarde (por ejemplo, la mayoría de los rieles).

Hay un par de cosas que puede hacer, todas están "bien" en el mejor de los casos. La mejor solución es encontrar la manera de deshacerse de los singletons. Esto es ondulado, lo sé, porque no hay una fórmula o algoritmo que pueda aplicar, y elimina mucha conveniencia, pero si puede hacerlo, a menudo vale la pena.

Si no puede hacerlo, al menos intente inyectar el singleton en lugar de acceder directamente a él. Las pruebas pueden ser difíciles en este momento, pero imagina tener que lidiar con problemas como este en tiempo de ejecución. Para eso, necesitaría una infraestructura integrada para manejarlo.

Aquí hay seis enfoques en los que he pensado.


proporcionar una instancia de la clase, pero permiten la clase a ser instanciado. Este es el más en línea con la forma tradicional en que se presentan los singletons. Básicamente, en cualquier momento que quiera referirse al singleton, usted habla con la instancia de singleton, pero puede probar contra otras instancias. Hay un módulo en el stdlib para ayudar con esto, pero hace que .new sea privado, por lo que si desea usarlo tendrá que usar algo como let(:config) { Configuration.send :new } para probarlo.

class Configuration 
    def self.instance 
    @instance ||= new 
    end 

    attr_writer :credentials_file 

    def credentials_file 
    @credentials_file || raise("credentials file not set") 
    end 
end 

describe Config do 
    let(:config) { Configuration.new } 

    specify '.instance always refers to the same instance' do 
    Configuration.instance.should be_a_kind_of Configuration 
    Configuration.instance.should equal Configuration.instance 
    end 

    describe 'credentials_file' do 
    specify 'it can be set/reset' do 
     config.credentials_file = 'abc' 
     config.credentials_file.should == 'abc' 
     config.credentials_file = 'def' 
     config.credentials_file.should == 'def' 
    end 

    specify 'raises an error if accessed before being initialized' do 
     expect { config.credentials_file }.to raise_error 'credentials file not set' 
    end 
    end 
end 

Entonces cualquier lugar que desee acceder a ella, utilice Configuration.instance


Hacer el singleton un instancia de alguna otra clase. Luego puede probar la otra clase aisladamente, y no necesita probar su singleton explícitamente.

class Counter 
    attr_accessor :count 

    def initialize 
    @count = 0 
    end 

    def count! 
    @count += 1 
    end 
end 

describe Counter do 
    let(:counter) { Counter.new } 
    it 'starts at zero' do 
    counter.count.should be_zero 
    end 

    it 'increments when counted' do 
    counter.count! 
    counter.count.should == 1 
    end 
end 

Luego, en su aplicación en alguna parte:

MyCounter = Counter.new 

Puede asegurarse de que no editar la clase principal, a continuación, sólo subclase que para sus pruebas:

class Configuration 
    class << self 
    attr_writer :credentials_file 
    end 

    def self.credentials_file 
    @credentials_file || raise("credentials file not set") 
    end 
end 

describe Config do 
    let(:config) { Class.new Configuration } 
    describe 'credentials_file' do 
    specify 'it can be set/reset' do 
     config.credentials_file = 'abc' 
     config.credentials_file.should == 'abc' 
     config.credentials_file = 'def' 
     config.credentials_file.should == 'def' 
    end 

    specify 'raises an error if accessed before being initialized' do 
     expect { config.credentials_file }.to raise_error 'credentials file not set' 
    end 
    end 
end 

Luego en su aplicación en algún lugar:

MyConfig = Class.new Configuration 

Asegúrese de que hay una manera de restablecer el Singleton. O más en general, deshacer todo lo que hagas. (por ejemplo, si puede registrar algún objeto con el singleton, entonces necesita poder anular el registro, en Rails, por ejemplo, cuando subclase Railtie, lo registra en una matriz, pero puede access the array and delete the item from it).

class Configuration 
    def self.reset 
    @credentials_file = nil 
    end 

    class << self 
    attr_writer :credentials_file 
    end 

    def self.credentials_file 
    @credentials_file || raise("credentials file not set") 
    end 
end 

RSpec.configure do |config| 
    config.before { Configuration.reset } 
end 

describe Config do 
    describe 'credentials_file' do 
    specify 'it can be set/reset' do 
     Configuration.credentials_file = 'abc' 
     Configuration.credentials_file.should == 'abc' 
     Configuration.credentials_file = 'def' 
     Configuration.credentials_file.should == 'def' 
    end 

    specify 'raises an error if accessed before being initialized' do 
     expect { Configuration.credentials_file }.to raise_error 'credentials file not set' 
    end 
    end 
end 

Clon la clase en vez de probar directamente. Esto salió de un gist que hice, básicamente editas el clon en lugar de la clase real.

class Configuration 
    class << self 
    attr_writer :credentials_file 
    end 

    def self.credentials_file 
    @credentials_file || raise("credentials file not set") 
    end 
end 

describe Config do 
    let(:configuration) { Configuration.clone } 

    describe 'credentials_file' do 
    specify 'it can be set/reset' do 
     configuration.credentials_file = 'abc' 
     configuration.credentials_file.should == 'abc' 
     configuration.credentials_file = 'def' 
     configuration.credentials_file.should == 'def' 
    end 

    specify 'raises an error if accessed before being initialized' do 
     expect { configuration.credentials_file }.to raise_error 'credentials file not set' 
    end 
    end 
end 

desarrollar el comportamiento en módulos de, a continuación, extender que en singleton. Here es un ejemplo un poco más complicado. Probablemente tendrías que buscar en los métodos self.included y self.extended si necesitas inicializar algunas variables en el objeto.

module ConfigurationBehaviour 
    attr_writer :credentials_file 
    def credentials_file 
    @credentials_file || raise("credentials file not set") 
    end 
end 

describe Config do 
    let(:configuration) { Class.new { extend ConfigurationBehaviour } } 

    describe 'credentials_file' do 
    specify 'it can be set/reset' do 
     configuration.credentials_file = 'abc' 
     configuration.credentials_file.should == 'abc' 
     configuration.credentials_file = 'def' 
     configuration.credentials_file.should == 'def' 
    end 

    specify 'raises an error if accessed before being initialized' do 
     expect { configuration.credentials_file }.to raise_error 'credentials file not set' 
    end 
    end 
end 

Luego, en su aplicación en alguna parte:

class Configuration 
    extend ConfigurationBehaviour 
end 
+0

¡Guau, excelente respuesta Joshua! Hay mucha comida para pensar allí. Estaba leyendo esta publicación del blog [Por qué los Singletons son malvados] (http://blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx) antes y llegué a la conclusión de que al usar el singleton el patrón fue una mala elección de diseño de mi parte. Si tuviera TDD'ed esta clase desde la concepción, no creo que lo hubiera hecho de esta manera. ¡Aprendí mi lección, los singletons son realmente difíciles de probar!Usaré una de tus recomendaciones cuando rediseño esta clase (¡y TDD!). ¡Muchas gracias! – thegreendroid

+0

En mi opinión, la respuesta a continuación responde mejor a la pregunta original y a mi pregunta sobre el restablecimiento de un singleton en un rspec. –

21

supongo que simplemente hacer esto solucionará su problema:

describe MySingleton, "#not_initialised" do 
    it "raises an exception" do 
    Singleton.__init__(MySingleton) 
    expect {MySingleton.get_something}.to raise_error(RuntimeError) 
    end 
end 

o incluso mejor agregar a antes de devolución de llamada:

describe MySingleton, "#not_initialised" do 
    before(:each) { Singleton.__init__(MySingleton) } 
end 
+0

Muy simple y funciona perfectamente, gracias –

+0

¡Gracias! ¡Muy útil para probar! – wrzasa

+0

muchas gracias! – Reck

0

para extraer una TL; DR de la bonita ya responder más arriba, para futuros visitantes perezosa como yo - Me pareció que para ser limpio y fácil:

Si tuviera esto antes

let(:thing) { MyClass.instance } 

hacer esto en vez

let(:thing) { MyClass.clone.instance } 
Cuestiones relacionadas