21

Rubyでシングルトンオブジェクトをリセットするにはどうすればよいですか?実際のコードでこれを実行したくないことはわかっていますが、単体テストについてはどうでしょうか。

これが私がRSpecテストでやろうとしていることです-

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

以前のテストの1つがシングルトンオブジェクトを初期化するため、失敗します。このリンクからIanWhiteのアドバイスに従ってみました。これは、基本的にシングルトンにモンキーパッチを適用してreset_instanceメソッドを提供しますが、未定義のメソッド「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でこれを行う最も慣用的な方法は何ですか?

4

3 に答える 3

30

これを行うだけで問題が解決すると思います:

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
于 2013-02-24T14:40:48.760 に答える
28

難しい質問です。シングルトンはラフです。部分的には、あなたが示している理由 (それをリセットする方法) と、部分的には、後で噛みつく傾向のある仮定 (たとえば、Rails のほとんど) を行うためです。

できることはいくつかありますが、せいぜい「大丈夫」です。最善の解決策は、シングルトンを取り除く方法を見つけることです。適用できる数式やアルゴリズムがなく、多くの利便性が失われるため、これは手の込んだものですが、それができれば、多くの場合価値があります。

それができない場合は、直接アクセスするのではなく、少なくともシングルトンを注入してみてください。テストは今は難しいかもしれませんが、実行時にこのような問題に対処しなければならないことを想像してみてください。そのためには、それを処理するためのインフラストラクチャが組み込まれている必要があります。

私が考えた6つの方法を紹介します。


クラスのインスタンスを提供しますが、クラスをインスタンス化できるようにします。これは、シングルトンが伝統的に提示されている方法と最も一致しています。基本的に、シングルトンを参照したいときはいつでもシングルトン インスタンスと対話しますが、他のインスタンスに対してテストできます。これを支援する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

シングルトンをリセットする方法があることを確認してください。または、より一般的には、実行したことを元に戻します。(たとえば、あるオブジェクトをシングルトンに登録できる場合は、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

クラスを直接テストする代わりに、クラスを複製します。これは私が作成した要点から出てきたもので、基本的には実際のクラスではなくクローンを編集します。

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

modules で動作を開発し、それをシングルトンに拡張します。もう少し複雑な例を次に示しますオブジェクトのいくつかの変数を初期化する必要がある場合は、おそらくself.includedandメソッドを調べる必要があります。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
于 2012-08-26T08:11:38.133 に答える