Cách tốt nhất để kiểm tra đơn vị các phương thức được bảo vệ và riêng tư trong Ruby là gì?


136

Cách tốt nhất để kiểm tra đơn vị các phương thức được bảo vệ và riêng tư trong Ruby, sử dụng Test::Unitkhung Ruby tiêu chuẩn là gì?

Tôi chắc chắn rằng ai đó sẽ lên tiếng và khẳng định một cách giáo điều rằng "bạn chỉ nên thử nghiệm các phương thức công khai; nếu nó cần thử nghiệm đơn vị, thì nó không nên là một phương pháp được bảo vệ hoặc riêng tư", nhưng tôi không thực sự quan tâm đến việc tranh luận về điều đó. Tôi đã có một số phương thức được bảo vệ hoặc riêng tư vì những lý do chính đáng và hợp lệ, những phương thức riêng tư / được bảo vệ này phức tạp vừa phải và các phương thức công khai trong lớp phụ thuộc vào các phương thức được bảo vệ / riêng tư này hoạt động chính xác, do đó tôi cần một cách để kiểm tra các phương pháp được bảo vệ / riêng tư.

Một điều nữa ... Tôi thường đặt tất cả các phương thức cho một lớp nhất định vào một tệp và đơn vị kiểm tra cho lớp đó trong một tệp khác. Lý tưởng nhất, tôi muốn tất cả các phép thuật thực hiện chức năng "kiểm tra đơn vị các phương thức được bảo vệ và riêng tư" này vào tệp kiểm tra đơn vị, chứ không phải tệp nguồn chính, để giữ cho tệp nguồn chính đơn giản và dễ hiểu nhất có thể.


Câu trả lời:


135

Bạn có thể bỏ qua việc đóng gói với phương thức gửi:

myobject.send(:method_name, args)

Đây là một 'tính năng' của Ruby. :)

Đã có cuộc tranh luận nội bộ trong quá trình phát triển Ruby 1.9, xem xét việc sendtôn trọng quyền riêng tư và send!bỏ qua nó, nhưng cuối cùng, không có gì thay đổi trong Ruby 1.9. Bỏ qua các ý kiến ​​dưới đây thảo luận send!và phá vỡ mọi thứ.


tôi nghĩ rằng việc sử dụng này đã bị thu hồi vào 1.9
Gene T

6
Tôi nghi ngờ họ sẽ thu hồi nó, vì họ đã ngay lập tức phá vỡ một số lượng lớn các dự án ruby
Orion Edwards

1
ruby 1.9 không phá vỡ mọi thứ.
jes5199

1
Chỉ cần lưu ý: Đừng bận tâm đến send!điều đó, nó đã bị thu hồi từ lâu, send/__send__có thể gọi các phương thức của tất cả khả năng hiển thị - redmine.ruby-lang.org/reposeocate/revision/1?rev=13824
dolzenko

2
public_send(tài liệu ở đây ) nếu bạn muốn tôn trọng quyền riêng tư. Tôi nghĩ rằng đó là mới đối với Ruby 1.9.
Andrew Grimm

71

Đây là một cách dễ dàng nếu bạn sử dụng RSpec:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end

9
Vâng, điều đó thật tuyệt. Đối với các phương thức riêng tư, hãy sử dụng ... private_instance_methods thay vì reserved_instance_methods
Mike Blyth

12
Thông báo trước quan trọng: điều này làm cho các phương thức trên lớp này được công khai trong phần còn lại của quá trình thực thi bộ thử nghiệm của bạn, có thể có tác dụng phụ không mong muốn! Bạn có thể muốn xác định lại các phương thức như được bảo vệ một lần nữa trong một khối sau (: mỗi) hoặc chịu các thử nghiệm ma quái trong tương lai.
Tác nhân gây bệnh

điều này thật kinh khủng và rực rỡ cùng một lúc
Robert

Tôi chưa bao giờ thấy điều này trước đây và tôi có thể chứng thực rằng nó hoạt động tuyệt vời. Đúng là nó vừa kinh khủng vừa xuất sắc nhưng miễn là bạn phạm vi nó ở cấp độ của phương pháp bạn đang thử nghiệm, tôi sẽ lập luận rằng bạn sẽ không có tác dụng phụ bất ngờ mà Pathogen ám chỉ.
nhóm mờ

32

Chỉ cần mở lại lớp trong tệp thử nghiệm của bạn và xác định lại phương thức hoặc phương thức là công khai. Bạn không cần phải xác định lại can đảm của phương thức, chỉ cần truyền biểu tượng vào publiccuộc gọi.

Nếu lớp gốc của bạn được định nghĩa như thế này:

class MyClass

  private

  def foo
    true
  end
end

Trong tệp kiểm tra của bạn, chỉ cần làm một cái gì đó như thế này:

class MyClass
  public :foo

end

Bạn có thể truyền nhiều biểu tượng cho publicnếu bạn muốn trưng ra các phương thức riêng tư hơn.

public :foo, :bar

2
Đây là cách tiếp cận ưa thích của tôi vì nó khiến mã của bạn không bị ảnh hưởng và chỉ cần điều chỉnh quyền riêng tư cho thử nghiệm cụ thể. Đừng quên đặt mọi thứ trở lại như sau khi các bài kiểm tra của bạn chạy hoặc bạn có thể làm hỏng các bài kiểm tra sau này.
ktec

10

instance_eval() có thể giúp:

--------------------------------------------------- Object#instance_eval
     obj.instance_eval(string [, filename [, lineno]] )   => obj
     obj.instance_eval {| | block }                       => obj
------------------------------------------------------------------------
     Evaluates a string containing Ruby source code, or the given 
     block, within the context of the receiver (obj). In order to set 
     the context, the variable self is set to obj while the code is 
     executing, giving the code access to obj's instance variables. In 
     the version of instance_eval that takes a String, the optional 
     second and third parameters supply a filename and starting line 
     number that are used when reporting compilation errors.

        class Klass
          def initialize
            @secret = 99
          end
        end
        k = Klass.new
        k.instance_eval { @secret }   #=> 99

Bạn có thể sử dụng nó để truy cập các phương thức riêng tư và các biến thể hiện trực tiếp.

Bạn cũng có thể cân nhắc sử dụng send(), điều này cũng sẽ cung cấp cho bạn quyền truy cập vào các phương thức riêng tư và được bảo vệ (như James Baker đề xuất)

Ngoài ra, bạn có thể sửa đổi siêu dữ liệu của đối tượng thử nghiệm của mình để đặt các phương thức riêng tư / được bảo vệ thành công khai cho đối tượng đó.

    test_obj.a_private_method(...) #=> raises NoMethodError
    test_obj.a_protected_method(...) #=> raises NoMethodError
    class << test_obj
        public :a_private_method, :a_protected_method
    end
    test_obj.a_private_method(...) # executes
    test_obj.a_protected_method(...) # executes

    other_test_obj = test.obj.class.new
    other_test_obj.a_private_method(...) #=> raises NoMethodError
    other_test_obj.a_protected_method(...) #=> raises NoMethodError

Điều này sẽ cho phép bạn gọi các phương thức này mà không ảnh hưởng đến các đối tượng khác của lớp đó. Bạn có thể mở lại lớp trong thư mục kiểm tra của mình và đặt chúng ở chế độ công khai cho tất cả các phiên bản trong mã kiểm tra của bạn, nhưng điều đó có thể ảnh hưởng đến việc kiểm tra giao diện chung của bạn.


9

Một cách tôi đã làm trong quá khứ là:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

8

Tôi chắc chắn rằng ai đó sẽ lên tiếng và khẳng định một cách giáo điều rằng "bạn chỉ nên thử nghiệm các phương thức công khai; nếu nó cần thử nghiệm đơn vị, thì nó không nên là một phương pháp được bảo vệ hoặc riêng tư", nhưng tôi không thực sự quan tâm đến việc tranh luận về điều đó.

Bạn cũng có thể cấu trúc lại chúng thành một đối tượng mới trong đó các phương thức đó là công khai và ủy thác cho chúng một cách riêng tư trong lớp gốc. Điều này sẽ cho phép bạn kiểm tra các phương thức mà không cần phép thuật siêu hình trong thông số kỹ thuật của bạn trong khi vẫn giữ chúng ở chế độ riêng tư.

Tôi đã có một số phương pháp được bảo vệ hoặc riêng tư vì những lý do chính đáng và hợp lệ

Những lý do hợp lệ là gì? Các ngôn ngữ OOP khác có thể thoát khỏi mà không cần phương thức riêng tư (smalltalk xuất hiện trong tâm trí - nơi các phương thức riêng tư chỉ tồn tại như một quy ước).


Đúng, nhưng hầu hết các Smalltalker không nghĩ rằng đó là một tính năng tốt của ngôn ngữ.
aenw

6

Tương tự như phản hồi của @ WillSargent, đây là những gì tôi đã sử dụng trong một describekhối cho trường hợp đặc biệt kiểm tra một số trình xác nhận được bảo vệ mà không cần phải trải qua quá trình tạo / cập nhật nặng nề với FactoryGirl (và bạn có thể sử dụng private_instance_methodstương tự):

  describe "protected custom `validates` methods" do
    # Test these methods directly to avoid needing FactoryGirl.create
    # to trigger before_create, etc.
    before(:all) do
      @protected_methods = MyClass.protected_instance_methods
      MyClass.send(:public, *@protected_methods)
    end
    after(:all) do
      MyClass.send(:protected, *@protected_methods)
      @protected_methods = nil
    end

    # ...do some tests...
  end

5

Để công khai tất cả phương thức được bảo vệ và riêng tư cho lớp được mô tả, bạn có thể thêm phần sau vào spec_helper.rb của mình và không phải chạm vào bất kỳ tệp spec nào.

RSpec.configure do |config|
  config.before(:each) do
    described_class.send(:public, *described_class.protected_instance_methods)
    described_class.send(:public, *described_class.private_instance_methods)
  end
end

3

Bạn có thể "mở lại" lớp và cung cấp một phương thức mới ủy nhiệm cho lớp riêng:

class Foo
  private
  def bar; puts "Oi! how did you reach me??"; end
end
# and then
class Foo
  def ah_hah; bar; end
end
# then
Foo.new.ah_hah

2

Tôi có lẽ sẽ nghiêng về việc sử dụng instance_eval (). Tuy nhiên, trước khi tôi biết về instance_eval (), tôi sẽ tạo một lớp dẫn xuất trong tệp thử nghiệm đơn vị của mình. Sau đó tôi sẽ đặt (các) phương thức riêng tư thành công khai.

Trong ví dụ dưới đây, phương thức build_year_range là riêng tư trong lớp PublicationSearch :: ISIQuery. Việc tạo ra một lớp mới chỉ nhằm mục đích thử nghiệm cho phép tôi đặt (các) phương thức thành công khai và do đó, có thể kiểm tra trực tiếp. Tương tự, lớp dẫn xuất hiển thị một biến thể hiện gọi là 'kết quả' mà trước đây không được hiển thị.

# A derived class useful for testing.
class MockISIQuery < PublicationSearch::ISIQuery
    attr_accessor :result
    public :build_year_range
end

Trong thử nghiệm đơn vị của tôi, tôi có một trường hợp thử nghiệm khởi tạo lớp MockISIQuery và trực tiếp kiểm tra phương thức build_year_range ().


2

Tôi biết tôi đến bữa tiệc muộn, nhưng đừng thử phương pháp riêng tư .... Tôi không thể nghĩ ra lý do để làm điều này. Một phương thức có thể truy cập công khai đang sử dụng phương thức riêng tư đó ở đâu đó, kiểm tra phương thức công khai và nhiều tình huống có thể khiến phương thức riêng tư đó được sử dụng. Một cái gì đó đi vào, một cái gì đó đi ra. Việc kiểm tra các phương thức riêng tư là một điều không nên và nó khiến cho việc tái cấu trúc mã của bạn sau này trở nên khó khăn hơn nhiều. Họ là riêng tư cho một lý do.


14
Vẫn không hiểu vị trí này: Có, các phương thức riêng tư là riêng tư vì một lý do, nhưng không, lý do này không liên quan gì đến thử nghiệm.
Sebastian nôn Meer

Tôi ước tôi có thể nâng cao điều này hơn nữa. Câu trả lời đúng duy nhất trong chủ đề này.
Psynix

2

Trong Test :: Khung đơn vị có thể viết,

MyClass.send(:public, :method_name)

Ở đây "method_name" là phương thức riêng tư.

& trong khi gọi phương thức này có thể viết,

assert_equal expected, MyClass.instance.method_name(params)

1

Đây là một bổ sung chung cho Class mà tôi sử dụng. Đó là một khẩu súng ngắn hơn một chút so với việc chỉ công khai phương pháp mà bạn đang thử nghiệm, nhưng trong hầu hết các trường hợp, nó không thành vấn đề, và nó dễ đọc hơn nhiều.

class Class
  def publicize_methods
    saved_private_instance_methods = self.private_instance_methods
    self.class_eval { public *saved_private_instance_methods }
    begin
      yield
    ensure
      self.class_eval { private *saved_private_instance_methods }
    end
  end
end

MyClass.publicize_methods do
  assert_equal 10, MyClass.new.secret_private_method
end

Sử dụng gửi đến khoá protected / phương pháp tin được chia nhỏ trong 1,9, do đó không phải là một giải pháp được khuyến nghị.


1

Để sửa câu trả lời hàng đầu ở trên: trong Ruby 1.9.1, đó là Object # send gửi tất cả các tin nhắn và Object # public_send tôn trọng quyền riêng tư.


1
Bạn nên thêm bình luận cho câu trả lời đó, không viết câu trả lời mới để sửa câu khác.
zishe

1

Thay vì obj.send, bạn có thể sử dụng phương thức singleton. Đó là thêm 3 dòng mã trong lớp thử nghiệm của bạn và không yêu cầu thay đổi mã thực tế để được kiểm tra.

def obj.my_private_method_publicly (*args)
  my_private_method(*args)
end

Trong các trường hợp kiểm tra, sau đó bạn sử dụng my_private_method_publiclybất cứ khi nào bạn muốn kiểm tra my_private_method.

http://mathandprogramming.blogspot.com/2010/01/ruby-testing-private-methods.html

obj.sendđối với các phương thức riêng tư đã được thay thế bằng send!1.9, nhưng sau đó send!lại bị xóa. Vì vậy, obj.sendhoạt động hoàn toàn tốt.


1

Để làm điều này:

disrespect_privacy @object do |p|
  assert p.private_method
end

Bạn có thể thực hiện điều này trong tệp test_helper của bạn:

class ActiveSupport::TestCase
  def disrespect_privacy(object_or_class, &block)   # access private methods in a block
    raise ArgumentError, 'Block must be specified' unless block_given?
    yield Disrespect.new(object_or_class)
  end

  class Disrespect
    def initialize(object_or_class)
      @object = object_or_class
    end
    def method_missing(method, *args)
      @object.send(method, *args)
    end
  end
end

Heh Tôi đã có một số vui vẻ với điều này: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 đổi tên Disrespectđể PrivacyViolator(: P) và thực hiện các disrespect_privacyphương pháp tạm thời chỉnh sửa các khối của ràng buộc, như vậy là để nhắc nhở các đối tượng mục tiêu để các đối tượng wrapper, nhưng chỉ trong thời gian của khối. Bằng cách đó, bạn không cần sử dụng thông số khối, bạn có thể tiếp tục tham chiếu đối tượng có cùng tên.
Alexander - Tái lập Monica
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.