Ответ 1
Жесткий вопрос, синглтоны грубые. Частично по той причине, что вы показываете (как reset it), и частично потому, что они делают предположения, которые имеют тенденцию укусить вас позже (например, большинство Rails).
Есть несколько вещей, которые вы можете сделать, они в лучшем случае "все в порядке". Лучшее решение - найти способ избавиться от одиночек. Я знаю, это ручная волна, потому что нет формулы или алгоритма, которые вы можете применить, и это удаляет много удобства, но если вы можете это сделать, это часто стоит.
Если вы не можете этого сделать, по крайней мере, попробуйте ввести синглтон, а не напрямую обращаться к нему. Тестирование может быть затруднено сейчас, но представьте себе, что нужно иметь дело с такими проблемами во время выполнения. Для этого вам понадобится инфраструктура, встроенная для ее обработки.
Вот шесть подходов, о которых я думал.
Предоставить экземпляр класса, но разрешить создание экземпляра. Это наиболее соответствует тому, как традиционно представлены синглтоны. В принципе, в любое время, когда вы хотите обратиться к singleton, вы разговариваете с экземпляром singleton, но можете протестировать другие экземпляры. Там в модуле stdlib есть модуль, но он делает .new
приватным, поэтому, если вы хотите его использовать, вам нужно будет использовать что-то вроде let(:config) { Configuration.send :new }
, чтобы проверить его.
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
Затем в любом месте, где вы хотите получить к нему доступ, используйте Configuration.instance
Создание одиночного элемента экземпляра другого класса. Затем вы можете протестировать другой класс отдельно и не нужно явно проверять свой синглтон.
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
Затем в вашем приложении где-то:
MyCounter = Counter.new
Вы можете быть уверены, что никогда не будете редактировать основной класс, а затем просто подкласс для своих тестов:
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
Затем в вашем приложении где-то:
MyConfig = Class.new Configuration
Убедитесь, что существует способ reset singleton. Или, в общем, отмените все, что вы делаете. (например, если вы можете зарегистрировать какой-либо объект с помощью singleton, тогда вам нужно иметь возможность его отменить, в Rails, например, при подклассе Railtie
он записывает это в массив, но вы можете получить доступ к массиву и удалить элемент из него).
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
Клонировать класс вместо прямого тестирования. Это произошло из gist, который я сделал, в основном вы редактируете клон вместо реального класса.
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
Разработайте поведение в модулях, затем продолжите это на singleton. Здесь - несколько более привлекательный пример. Вероятно, вам нужно будет изучить методы self.included
и self.extended
, если вам нужно инициализировать некоторые переменные на объекте.
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
Затем в вашем приложении где-то:
class Configuration
extend ConfigurationBehaviour
end