2012-08-26 22 views
18

Làm cách nào để đặt lại một đối tượng đơn lẻ trong Ruby? Tôi biết rằng một người không bao giờ muốn làm điều này trong mã số thực sự nhưng về thử nghiệm đơn vị thì sao?Đặt lại một cá thể đơn trong Ruby

Đây là những gì tôi đang cố gắng để làm trong một thử nghiệm RSpec -

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

Nó thất bại vì một trong những thử nghiệm trước đây của tôi initialises đối tượng singleton. Tôi đã cố gắng theo lời khuyên của Ian White từ liên kết this mà về cơ bản bản vá lỗi khỉ Singleton cung cấp phương thức reset_instance nhưng tôi nhận được một ngoại lệ 'reset_instance' chưa được xác định.

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 

Cách thành ngữ nhất để làm điều này trong Ruby là gì?

Trả lời

25

Câu hỏi khó, người đơn thô. Một phần vì lý do bạn đang hiển thị (cách đặt lại), và một phần vì chúng tạo ra các giả định có khuynh hướng cắn bạn sau này (ví dụ: hầu hết Rails).

Có một vài điều bạn có thể làm, tất cả chúng đều "ổn" ở mức tốt nhất. Giải pháp tốt nhất là tìm cách để loại bỏ những người độc thân. Điều này là lượn sóng, tôi biết, bởi vì không có một công thức hay thuật toán bạn có thể áp dụng, và nó loại bỏ rất nhiều tiện lợi, nhưng nếu bạn có thể làm điều đó, nó thường đáng giá.

Nếu bạn không thể làm điều đó, ít nhất hãy thử tiêm singleton thay vì truy cập trực tiếp. Thử nghiệm có thể khó khăn ngay bây giờ, nhưng hãy tưởng tượng phải đối phó với các vấn đề như thế này khi chạy. Để làm được điều đó, bạn cần có cơ sở hạ tầng được xây dựng để xử lý nó.

Dưới đây là sáu cách tiếp cận mà tôi đã nghĩ đến.


Cung cấp một thể hiện của lớp, nhưng cho phép các lớp học để được instantiated. Điều này là phù hợp nhất với cách thức những người độc thân được trình bày theo truyền thống. Về cơ bản bất cứ lúc nào bạn muốn tham khảo singleton, bạn nói chuyện với trường hợp singleton, nhưng bạn có thể kiểm tra đối với các trường hợp khác. Có một mô-đun trong số stdlib để trợ giúp điều này, nhưng nó làm cho .new riêng tư, vì vậy nếu bạn muốn sử dụng nó, bạn phải sử dụng một cái gì đó như let(:config) { Configuration.send :new } để kiểm tra nó.

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 

Sau đó, bất cứ nơi nào bạn muốn truy cập nó, sử dụng Configuration.instance


Lập singleton một thể hiện của một số lớp khác. Sau đó, bạn có thể kiểm tra các lớp khác trong sự cô lập, và không cần phải kiểm tra singleton của bạn một cách rõ ràng.

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 

Sau đó, trong ứng dụng của bạn ở đâu đó:

MyCounter = Counter.new 

Bạn có thể chắc chắn rằng không bao giờ sửa lại lớp chính, sau đó chỉ cần lớp con nó cho các bài kiểm tra của bạn:

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 

Sau đó, trong ứng dụng của bạn ở đâu đó:

MyConfig = Class.new Configuration 

Đảm bảo rằng có một cách để reset singleton. Hoặc nói chung, hoàn tác mọi thứ bạn làm. (ví dụ: nếu bạn có thể đăng ký một số đối tượng với singleton, thì bạn cần phải có thể hủy đăng ký đối tượng đó, chẳng hạn như khi bạn phân lớp Railtie, nó ghi lại trong một mảng, nhưng bạn có thể 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 

Clone lớp thay vì thử nghiệm nó trực tiếp. Điều này xuất phát từ một số gist Tôi đã thực hiện, về cơ bản bạn chỉnh sửa bản sao thay vì lớp thực.

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 

Phát triển hành vi trong module, sau đó mở rộng đó vào singleton. Here là một ví dụ liên quan nhiều hơn một chút. Có lẽ bạn sẽ phải xem xét các phương thức self.includedself.extended nếu bạn cần khởi tạo một số biến trên đối tượng.

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 

Sau đó, trong ứng dụng của bạn ở đâu đó:

class Configuration 
    extend ConfigurationBehaviour 
end 
+0

Wow, câu trả lời tuyệt vời Joshua! Có rất nhiều thức ăn để suy nghĩ ở đó. Tôi đã đọc bài đăng trên blog này [Tại sao Singletons là Evil] (http://blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx) trước đây và tôi đi đến kết luận rằng sử dụng singleton mô hình là một sự lựa chọn thiết kế xấu trên một phần của tôi. Nếu tôi đã TDD'ed lớp này từ quan niệm, tôi không nghĩ rằng tôi đã làm nó theo cách này. Tôi đã học được bài học của tôi, những người độc thân thực sự khó kiểm tra!Tôi sẽ sử dụng một trong các đề xuất của bạn khi tôi thiết kế lại lớp này (và TDD nó!). Cảm ơn rất nhiều! – thegreendroid

+0

Theo ý kiến ​​của tôi, câu trả lời dưới đây sẽ trả lời tốt hơn câu hỏi gốc và câu hỏi của tôi về việc đặt lại một singleton trong một rspec. –

21

Tôi đoán chỉ đơn giản là làm được điều này sẽ khắc phục vấn đề của bạn:

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

hoặc thậm chí thêm tốt hơn để trước khi gọi lại:

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

Rất đơn giản và hoạt động hoàn hảo, cảm ơn –

+0

Cảm ơn! Rất hữu ích để thử nghiệm! – wrzasa

+0

cảm ơn bạn rất nhiều! – Reck

0

Để trích xuất một TL; DR từ đẹp còn trả lời ở trên, dành cho du khách lười biếng trong tương lai như tôi - Tôi thấy điều này phải được làm sạch và dễ dàng:

Nếu bạn có điều này trước khi

let(:thing) { MyClass.instance } 

Làm điều này thay vì

let(:thing) { MyClass.clone.instance } 
Các vấn đề liên quan