Apa cara terbaik untuk menguji metode protected & private di Ruby?

137

Apa cara terbaik untuk menguji unit metode terproteksi dan privat di Ruby, menggunakan Test::Unitkerangka kerja Ruby standar ?

Saya yakin seseorang akan menyalurkan dan secara dogmatis menegaskan bahwa "Anda hanya harus menguji metode publik; jika perlu pengujian unit, itu tidak boleh menjadi metode yang dilindungi atau pribadi", tetapi saya tidak terlalu tertarik untuk memperdebatkannya. Aku punya beberapa metode yang sedang dilindungi atau swasta untuk kebaikan dan alasan yang sah, / metode ini swasta dilindungi yang cukup kompleks, dan metode umum di kelas tergantung dilindungi / metode ini swasta berfungsi dengan benar, karena itu saya perlu cara untuk menguji metode dilindungi / pribadi.

Satu hal lagi ... Saya biasanya meletakkan semua metode untuk kelas tertentu dalam satu file, dan tes unit untuk kelas itu di file lain. Idealnya, saya ingin semua keajaiban untuk mengimplementasikan fungsi "uji unit metode dilindungi dan pribadi" ini ke dalam file pengujian unit, bukan file sumber utama, untuk menjaga file sumber utama sesederhana dan sesederhana mungkin.

Brent Chapman
sumber

Jawaban:

138

Anda dapat melewati enkapsulasi dengan metode kirim:

myobject.send(:method_name, args)

Ini adalah 'fitur' Ruby. :)

Ada perdebatan internal selama pengembangan Ruby 1.9 yang menganggap sendmenghargai privasi dan send!mengabaikannya, tetapi pada akhirnya tidak ada yang berubah di Ruby 1.9. Abaikan komentar di bawah ini membahas send!dan memecahkan sesuatu.

James Baker
sumber
saya pikir penggunaan ini telah dicabut pada 1.9
Gene T
6
Saya ragu mereka akan mencabutnya, karena mereka akan langsung merusak sejumlah besar proyek ruby
Orion Edwards
1
ruby 1,9 tidak pecah saja.
jes5199
1
Hanya untuk catatan: Jangankan send!hal, itu dicabut lama, send/__send__dapat memanggil metode dari semua visibilitas - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko
2
Ada public_send(dokumentasi di sini ) jika Anda ingin menghormati privasi. Saya pikir itu baru di Ruby 1.9.
Andrew Grimm
71

Berikut salah satu cara mudah jika Anda menggunakan RSpec:

before(:each) do
  MyClass.send(:public, *MyClass.protected_instance_methods)  
end
Will Sargent
sumber
9
Iya itu bagus. Untuk metode privat, gunakan ... private_instance_methods daripada protected_instance_methods
Mike Blyth
12
Peringatan penting: ini membuat metode pada kelas ini menjadi publik selama sisa eksekusi rangkaian pengujian Anda, yang dapat memiliki efek samping yang tidak terduga! Anda mungkin ingin mendefinisikan kembali metode sebagai dilindungi lagi di blok setelah (: masing-masing) atau mengalami kegagalan pengujian yang menyeramkan di masa mendatang.
Patogen
ini mengerikan dan brilian pada saat yang sama
Robert
Saya belum pernah melihat ini sebelumnya dan saya dapat membuktikan bahwa ini bekerja dengan luar biasa. Ya, ini mengerikan dan brilian, tetapi selama Anda mengukurnya pada tingkat metode yang Anda uji, saya berpendapat bahwa Anda tidak akan memiliki efek samping yang tidak terduga seperti yang disinggung oleh Patogen.
fuzzygroup
32

Buka kembali kelas di file pengujian Anda, dan tentukan ulang metode atau metode sebagai publik. Anda tidak perlu mendefinisikan ulang keberanian dari metode itu sendiri, cukup teruskan simbol ke dalam publicpanggilan.

Jika Anda kelas asli didefinisikan seperti ini:

class MyClass

  private

  def foo
    true
  end
end

Di file pengujian Anda, lakukan saja seperti ini:

class MyClass
  public :foo

end

Anda dapat memberikan beberapa simbol publicjika Anda ingin mengekspos metode yang lebih pribadi.

public :foo, :bar
Aaron Hinni
sumber
2
Ini adalah pendekatan pilihan saya karena membiarkan kode Anda tidak tersentuh dan hanya menyesuaikan privasi untuk pengujian tertentu. Jangan lupa untuk mengembalikan semuanya seperti semula setelah tes Anda berjalan atau Anda mungkin merusak tes selanjutnya.
ktec
10

instance_eval() mungkin membantu:

--------------------------------------------------- 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

Anda dapat menggunakannya untuk mengakses metode privat dan variabel instance secara langsung.

Anda juga dapat mempertimbangkan untuk menggunakan send(), yang juga akan memberi Anda akses ke metode pribadi dan dilindungi (seperti yang disarankan James Baker)

Alternatifnya, Anda bisa memodifikasi metaclass objek pengujian Anda untuk membuat metode privat / dilindungi publik hanya untuk objek itu.

    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

Ini akan memungkinkan Anda memanggil metode ini tanpa memengaruhi objek lain dari kelas itu. Anda dapat membuka kembali kelas dalam direktori pengujian Anda dan menjadikannya publik untuk semua instance dalam kode pengujian Anda, tetapi hal itu mungkin memengaruhi pengujian Anda terhadap antarmuka publik.

rampion
sumber
9

Salah satu cara saya melakukannya di masa lalu adalah:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end
Scott
sumber
8

Saya yakin seseorang akan menyalurkan dan secara dogmatis menegaskan bahwa "Anda hanya harus menguji metode umum; jika perlu pengujian unit, itu tidak boleh menjadi metode yang dilindungi atau pribadi", tetapi saya tidak terlalu tertarik untuk memperdebatkannya.

Anda juga dapat memfaktor ulang mereka menjadi objek baru di mana metode tersebut bersifat publik, dan mendelegasikannya secara pribadi di kelas asli. Ini akan memungkinkan Anda untuk menguji metode tanpa metaruby ajaib dalam spesifikasi Anda sambil tetap merahasiakannya.

Saya punya beberapa metode yang dilindungi atau pribadi untuk alasan yang baik dan valid

Apa alasan yang valid itu? Bahasa OOP lain bisa lolos tanpa metode privat sama sekali (pembicaraan kecil muncul di benak - di mana metode privat hanya ada sebagai konvensi).

pengguna52804
sumber
Ya, tapi kebanyakan Smalltalker tidak menganggap itu fitur yang bagus dari bahasa tersebut.
aenw
6

Mirip dengan tanggapan @ WillSargent, inilah yang telah saya gunakan dalam sebuah describeblok untuk kasus khusus menguji beberapa validator yang dilindungi tanpa perlu melalui proses kelas berat untuk membuat / memperbaruinya dengan FactoryGirl (dan Anda dapat menggunakannya dengan private_instance_methodscara yang sama):

  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
qix
sumber
5

Agar publik semua metode dilindungi dan privat untuk kelas yang dijelaskan, Anda dapat menambahkan berikut ini ke spec_helper.rb Anda dan tidak perlu menyentuh file spesifikasi Anda.

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
Sean Tan
sumber
4

Anda dapat "membuka kembali" kelas dan memberikan metode baru yang mendelegasikan ke kelas pribadi:

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
tragomaskhalos
sumber
2

Saya mungkin akan cenderung menggunakan instance_eval (). Sebelum saya tahu tentang instance_eval (), saya akan membuat kelas turunan di file pengujian unit saya. Saya kemudian akan mengatur metode privat menjadi publik.

Pada contoh di bawah ini, metode build_year_range bersifat privat di kelas PublicationSearch :: ISIQuery. Mendapatkan kelas baru hanya untuk tujuan pengujian memungkinkan saya untuk menyetel metode menjadi publik dan, oleh karena itu, dapat diuji secara langsung. Demikian juga, kelas turunan mengekspos variabel instan yang disebut 'hasil' yang sebelumnya tidak diekspos.

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

Dalam pengujian unit saya, saya memiliki kasus uji yang membuat instance kelas MockISIQuery dan langsung menguji metode build_year_range ().

Mike
sumber
2

Dalam Test :: Kerangka unit dapat menulis,

MyClass.send(:public, :method_name)

Di sini "method_name" adalah metode pribadi.

& sambil memanggil metode ini dapat menulis,

assert_equal expected, MyClass.instance.method_name(params)
rahul patil
sumber
1

Berikut adalah tambahan umum untuk Kelas yang saya gunakan. Ini sedikit lebih banyak daripada hanya mempublikasikan metode yang Anda uji, tetapi dalam banyak kasus itu tidak masalah, dan itu jauh lebih mudah dibaca.

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

Menggunakan kirim ke akses yang dilindungi / metode swasta yang rusak di 1.9, jadi bukan solusi yang direkomendasikan.


sumber
1

Untuk mengoreksi jawaban teratas di atas: di Ruby 1.9.1, Object # sendlah yang mengirim semua pesan, dan Object # public_send yang menghormati privasi.

Victor K.
sumber
1
Anda harus menambahkan komentar ke jawaban itu, bukan menulis jawaban baru untuk mengoreksi yang lain.
zishe
1

Dari pada obj.send Anda bisa menggunakan metode tunggal. Ini adalah 3 baris kode lagi di kelas pengujian Anda dan tidak memerlukan perubahan pada kode sebenarnya untuk diuji.

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

Dalam kasus pengujian, Anda kemudian menggunakan my_private_method_publiclykapan pun Anda ingin menguji my_private_method.

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

obj.senduntuk metode privat digantikan oleh send!1.9, tapi kemudian send!dihapus lagi. Jadi obj.sendbekerja dengan baik.

Franz Hinkel
sumber
1

Saya tahu saya terlambat ke pesta, tetapi jangan menguji metode pribadi .... Saya tidak dapat memikirkan alasan untuk melakukan ini. Metode yang dapat diakses publik menggunakan metode privat tersebut di suatu tempat, menguji metode publik dan berbagai skenario yang akan menyebabkan metode privat tersebut digunakan. Sesuatu masuk, sesuatu keluar. Menguji metode privat adalah hal yang tidak boleh dilakukan, dan akan semakin sulit untuk memfaktorkan ulang kode Anda nanti. Mereka bersifat pribadi karena suatu alasan.

Logika Biner
sumber
14
Masih tidak memahami posisi ini: Ya, metode privat bersifat privat karena suatu alasan, tetapi tidak, alasan ini tidak ada hubungannya dengan pengujian.
Sebastian vom Meer
Saya berharap saya bisa lebih suka ini. Satu-satunya jawaban yang benar di utas ini.
Psynix
Jika Anda memiliki sudut pandang itu lalu mengapa repot-repot dengan tes unit? Cukup tulis spesifikasi fitur: masukan masuk, halaman keluar, semua yang ada di antaranya harus dibahas, bukan?
ohhh
1

Untuk melakukan ini:

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

Anda dapat menerapkan ini di file test_helper Anda:

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
Knut Stenmark
sumber
Heh saya bersenang-senang dengan ini: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82 Berganti nama Disrespectmenjadi PrivacyViolator(: P) dan membuat disrespect_privacymetode untuk sementara mengedit pengikatan blok, untuk mengingatkan objek target ke objek pembungkus, tetapi hanya untuk durasi dari blok. Dengan begitu Anda tidak perlu menggunakan parameter blok, Anda dapat terus mereferensikan objek dengan nama yang sama.
Alexander - Kembalikan Monica