難しい質問です。シングルトンはラフです。部分的には、あなたが示している理由 (それをリセットする方法) と、部分的には、後で噛みつく傾向のある仮定 (たとえば、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.included
andメソッドを調べる必要があります。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