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