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
¡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
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. –