tutorial mvc mediante framework español desarrollo arquitectura aplicaciones ruby rspec singleton rspec2

ruby - mediante - spring mvc 4 tutorial español



Reiniciando una instancia de singleton en Ruby (3)

Para extraer un TL; DR de la agradable respuesta más larga anterior, para futuros visitantes perezosos como yo, encontré que esto es limpio y fácil:

Si tuvieras esto antes

let(:thing) { MyClass.instance }

Haz esto en lugar

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

¿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?

Esto es lo que estoy tratando de hacer en una prueba de RSpec:

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

Falla porque una de mis pruebas anteriores inicializa el objeto singleton. He intentado seguir los consejos de Ian White en this enlace, que esencialmente parlan parches Singleton para proporcionar un método reset_instance pero obtengo una excepción de método indefinido ''reset_instance''.

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?


Pregunta difícil, los singletons son ásperos. En parte por la razón 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 puedes hacer, todas están "bien" en el mejor de los casos. La mejor solución es encontrar una manera de deshacerse de singletons. Esto es mano-ondulada, lo sé, porque no hay una fórmula o un algoritmo que puedas aplicar, y elimina mucha conveniencia, pero si puedes hacerlo, a menudo vale la pena.

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

Aquí hay seis enfoques que he pensado.

Proporcione una instancia de la clase, pero permita que se cree una instancia de la clase . Esto es lo más en línea con la forma en que tradicionalmente se presentan los singletons. Básicamente, en cualquier momento en que quiera referirse al singleton, hable con la instancia de singleton, pero puede probar contra otros casos. Hay un módulo en stdlib para ayudar con esto, pero hace que .new sea ​​privado, así que si quieres usarlo, deberías 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

Luego, en cualquier lugar donde quiera acceder, use Configuration.instance

Haciendo el singleton una instancia de alguna otra clase . Luego puede probar la otra clase de forma aislada, 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 tu aplicación en algún lugar:

MyCounter = Counter.new

Puede asegurarse de nunca editar la clase principal, luego, haga una subclase 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 tu aplicación en algún lugar:

MyConfig = Class.new Configuration

Asegúrese de que hay una manera de restablecer el singleton . O más generalmente, deshacer cualquier cosa que hagas. (p. ej., si puede registrar algún objeto con el singleton, entonces debe poder anular el registro, en Rails, por ejemplo, cuando subclase Railtie , lo registra en una matriz, pero puede acceder a la matriz y eliminar el elemento). de ella ).

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

Clona la clase en lugar de probarlo directamente. Esto surgió de una 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

Desarrolle el comportamiento en módulos , luego extiéndalo sobre singleton. Here hay un ejemplo un poco más complicado. Probablemente tendrías que buscar en los métodos self.extended 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 tu aplicación en algún lugar:

class Configuration extend ConfigurationBehaviour end


Supongo que simplemente hacer esto solucionará tu 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 antes de devolución de llamada:

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