Подтвердить что ты не робот

Сброс экземпляра singleton в Ruby

Как reset объект singleton в Ruby? Я знаю, что вы никогда не хотели этого делать в реальном коде, но как насчет модульных тестов?

Вот что я пытаюсь сделать в RSpec-тесте -

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

Это не удается, потому что один из моих предыдущих тестов инициализирует объект singleton. Я пробовал следовать рекомендациям Яна Уайта из этой ссылки, в которой, по сути, обезьяны исправляют Singleton, чтобы предоставить метод reset_instance, но я получаю метод undefined '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

Каков самый идиоматический способ сделать это в Ruby?

4b9b3361

Ответ 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

Ответ 2

Я думаю, просто это исправит вашу проблему:

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

или даже лучше добавить перед обратным вызовом:

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

Ответ 3

Чтобы извлечь TL, DR из более приятного более длинного ответа выше, для будущих ленивых посетителей, таких как я, я нашел это чистым и легким:

Если у вас было это раньше

let(:thing) { MyClass.instance }

Сделайте это вместо

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