วิธีใดที่ดีที่สุดในการทดสอบหน่วยการป้องกันและวิธีการส่วนตัวใน Ruby


137

วิธีใดที่ดีที่สุดในการทดสอบหน่วยที่มีการป้องกันและวิธีการส่วนตัวใน Ruby โดยใช้Test::Unitกรอบมาตรฐาน Ruby

ฉันแน่ใจว่าจะมีใครสักคนพูดขึ้นและยืนยันโดยดันทุรังว่า "คุณควรทดสอบเฉพาะวิธีการสาธารณะเท่านั้นหากจำเป็นต้องมีการทดสอบหน่วยก็ไม่ควรเป็นวิธีการป้องกันหรือเป็นส่วนตัว" แต่ฉันไม่ได้สนใจที่จะถกเถียงเรื่องนี้จริงๆ ฉันมีวิธีการหลายอย่างที่ได้รับการป้องกันหรือเป็นส่วนตัวด้วยเหตุผลที่ดีและถูกต้องวิธีการส่วนตัว / การป้องกันเหล่านี้มีความซับซ้อนพอสมควรและวิธีการสาธารณะในชั้นเรียนขึ้นอยู่กับวิธีการป้องกัน / ส่วนตัวเหล่านี้ที่ทำงานได้อย่างถูกต้องดังนั้นฉันจึงต้องการวิธีทดสอบ วิธีการป้องกัน / ส่วนตัว

อีกอย่าง ... โดยทั่วไปฉันจะใส่วิธีการทั้งหมดสำหรับคลาสที่กำหนดไว้ในไฟล์เดียวและหน่วยทดสอบสำหรับคลาสนั้นในไฟล์อื่น ตามหลักการแล้วฉันต้องการให้เวทมนตร์ทั้งหมดใช้ฟังก์ชัน "การทดสอบหน่วยของวิธีการป้องกันและวิธีการส่วนตัว" นี้ในไฟล์ทดสอบหน่วยไม่ใช่ไฟล์ต้นฉบับหลักเพื่อให้ไฟล์ต้นฉบับหลักเรียบง่ายและตรงไปตรงมาที่สุด


คำตอบ:


138

คุณสามารถข้ามการห่อหุ้มด้วยวิธีการส่ง:

myobject.send(:method_name, args)

นี่คือ 'คุณลักษณะ' ของ Ruby :)

มีการถกเถียงกันภายในระหว่างการพัฒนา Ruby 1.9 ซึ่งถือว่าsendเคารพความเป็นส่วนตัวและsend!เพิกเฉยต่อมัน แต่สุดท้ายก็ไม่มีอะไรเปลี่ยนแปลงใน Ruby 1.9 ไม่สนใจความคิดเห็นด้านล่างที่พูดคุยsend!และทำลายสิ่งต่างๆ


ฉันคิดว่าการใช้งานนี้ถูกเพิกถอนใน 1.9
Gene T

6
ฉันสงสัยว่าพวกเขาจะเพิกถอนเพราะพวกเขาทำลายโครงการทับทิมจำนวนมหาศาลในทันที
Orion Edwards

1
ทับทิม 1.9 ไม่หยุดพักเพียงแค่ทุกอย่างเกี่ยวกับ
jes5199

1
หมายเหตุ: ไม่เป็นไรsend!สิ่งนี้ถูกเพิกถอนเมื่อนานมาแล้วsend/__send__สามารถเรียกวิธีการมองเห็นทั้งหมดได้ - redmine.ruby-lang.org/repositories/revision/1?rev=13824
dolzenko

2
มีpublic_send(เอกสารประกอบที่นี่ ) หากคุณต้องการเคารพความเป็นส่วนตัว ฉันคิดว่ามันยังใหม่สำหรับ Ruby 1.9
Andrew Grimm

71

นี่เป็นวิธีง่ายๆวิธีหนึ่งหากคุณใช้ RSpec:

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

9
ใช่มันเยี่ยมมาก สำหรับวิธีการส่วนตัวให้ใช้ ... private_instance_methods แทนที่จะเป็น protected_instance_methods
Mike Blyth

12
ข้อแม้สำคัญ: ทำให้เมธอดในคลาสนี้เป็นแบบสาธารณะในช่วงเวลาที่เหลือของการดำเนินการชุดทดสอบของคุณซึ่งอาจมีผลข้างเคียงที่ไม่คาดคิด! คุณอาจต้องการกำหนดวิธีการใหม่ที่ได้รับการป้องกันอีกครั้งในบล็อก after (: each) หรือประสบความล้มเหลวในการทดสอบที่น่ากลัวในอนาคต
เชื้อโรค

นี่เป็นสิ่งที่น่ากลัวและยอดเยี่ยมในเวลาเดียวกัน
Robert

ฉันไม่เคยเห็นสิ่งนี้มาก่อนและฉันสามารถยืนยันได้ว่ามันทำงานได้อย่างยอดเยี่ยม ใช่มันทั้งน่ากลัวและยอดเยี่ยม แต่ตราบใดที่คุณกำหนดขอบเขตในระดับของวิธีการที่คุณกำลังทดสอบฉันขอยืนยันว่าคุณจะไม่มีผลข้างเคียงที่ไม่คาดคิดที่ Pathogen พูดถึง
fuzzygroup

32

เพียงแค่เปิดชั้นเรียนขึ้นมาใหม่ในไฟล์ทดสอบของคุณและกำหนดวิธีการใหม่เป็นแบบสาธารณะ คุณไม่จำเป็นต้องกำหนดความกล้าของวิธีการใหม่เพียงแค่ส่งสัญลักษณ์เข้าไปในการpublicโทร

หากคลาสดั้งเดิมของคุณถูกกำหนดไว้เช่นนี้:

class MyClass

  private

  def foo
    true
  end
end

ในไฟล์ทดสอบของคุณให้ทำสิ่งนี้:

class MyClass
  public :foo

end

คุณสามารถส่งผ่านสัญลักษณ์หลายตัวไปได้publicหากคุณต้องการแสดงวิธีการส่วนตัวเพิ่มเติม

public :foo, :bar

2
นี่เป็นแนวทางที่ฉันต้องการเนื่องจากไม่ได้แตะต้องรหัสของคุณและปรับความเป็นส่วนตัวสำหรับการทดสอบเฉพาะ อย่าลืมใส่สิ่งต่างๆกลับไปเหมือนเดิมหลังจากการทดสอบของคุณดำเนินไปแล้วมิฉะนั้นการทดสอบในภายหลังอาจเสียหาย
ktec

10

instance_eval() อาจช่วยได้:

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

คุณสามารถใช้เพื่อเข้าถึงเมธอดส่วนตัวและตัวแปรอินสแตนซ์โดยตรง

คุณสามารถพิจารณาใช้send()ซึ่งจะช่วยให้คุณเข้าถึงวิธีการส่วนตัวและได้รับการป้องกัน (เช่นที่ James Baker แนะนำ)

หรือคุณสามารถปรับเปลี่ยน metaclass ของวัตถุทดสอบของคุณเพื่อทำให้เมธอดส่วนตัว / ป้องกันเป็นสาธารณะสำหรับออบเจ็กต์นั้นเท่านั้น

    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

วิธีนี้จะช่วยให้คุณสามารถเรียกใช้เมธอดเหล่านี้ได้โดยไม่ส่งผลกระทบต่อออบเจ็กต์อื่น ๆ ของคลาสนั้น คุณสามารถเปิดคลาสอีกครั้งภายในไดเร็กทอรีการทดสอบของคุณและทำให้เป็นแบบสาธารณะสำหรับอินสแตนซ์ทั้งหมดภายในโค้ดทดสอบของคุณ แต่อาจส่งผลต่อการทดสอบอินเทอร์เฟซสาธารณะของคุณ


9

วิธีหนึ่งที่ฉันเคยทำในอดีตคือ:

class foo
  def public_method
    private_method
  end

private unless 'test' == Rails.env

  def private_method
    'private'
  end
end

8

ฉันแน่ใจว่าจะมีใครสักคนพูดขึ้นและยืนยันโดยดันทุรังว่า "คุณควรทดสอบเฉพาะวิธีการสาธารณะเท่านั้นหากจำเป็นต้องมีการทดสอบหน่วยก็ไม่ควรเป็นวิธีการป้องกันหรือเป็นส่วนตัว" แต่ฉันไม่ได้สนใจที่จะถกเถียงเรื่องนี้จริงๆ

คุณยังสามารถ refactor เหล่านั้นให้เป็นอ็อบเจ็กต์ใหม่ซึ่งเมธอดเหล่านั้นเป็นแบบสาธารณะและมอบหมายให้กับคลาสเดิมแบบส่วนตัว วิธีนี้จะช่วยให้คุณสามารถทดสอบวิธีการที่ไม่มีเมตารูบี้เวทย์มนตร์ในข้อกำหนดของคุณในขณะที่ยังคงรักษาความเป็นส่วนตัว

ฉันมีหลายวิธีที่ได้รับการปกป้องหรือเป็นส่วนตัวด้วยเหตุผลที่ดีและถูกต้อง

อะไรคือเหตุผลที่ถูกต้องเหล่านั้น? ภาษา OOP อื่น ๆ สามารถหายไปได้โดยไม่ต้องใช้วิธีการส่วนตัวเลย (เป็นเรื่องเล็ก ๆ น้อย ๆ - ในกรณีที่วิธีการส่วนตัวมีอยู่เป็นแบบแผนเท่านั้น)


ใช่ แต่ Smalltalkers ส่วนใหญ่ไม่คิดว่านั่นเป็นคุณสมบัติที่ดีของภาษา
aenw

6

คล้ายกับคำตอบของ @ WillSargent นี่คือสิ่งที่ฉันใช้ในdescribeบล็อกสำหรับกรณีพิเศษในการทดสอบตัวตรวจสอบความถูกต้องที่ได้รับการป้องกันโดยไม่จำเป็นต้องผ่านขั้นตอนการสร้าง / อัปเดตด้วย FactoryGirl (และคุณสามารถใช้private_instance_methodsในลักษณะเดียวกันได้):

  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

ในการทำให้วิธีการที่ได้รับการป้องกันและเป็นส่วนตัวเป็นแบบสาธารณะสำหรับคลาสที่อธิบายไว้คุณสามารถเพิ่มสิ่งต่อไปนี้ใน spec_helper.rb ของคุณและไม่ต้องแตะไฟล์ข้อมูลจำเพาะใด ๆ ของคุณ

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

4

คุณสามารถ "เปิดใหม่" ชั้นเรียนและจัดเตรียมวิธีการใหม่ที่มอบสิทธิ์ให้กับคลาสส่วนตัว:

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

ฉันอาจจะโน้มตัวไปใช้ instance_eval () อย่างไรก็ตามก่อนที่ฉันจะรู้เรื่อง instance_eval () ฉันจะสร้างคลาสที่ได้รับมาในไฟล์ทดสอบหน่วยของฉัน จากนั้นฉันจะตั้งค่าวิธีการส่วนตัวเป็นสาธารณะ

ในตัวอย่างด้านล่างเมธอด build_year_range เป็นแบบส่วนตัวในคลาส PublicationSearch :: ISIQuery การได้รับคลาสใหม่เพียงเพื่อจุดประสงค์ในการทดสอบทำให้ฉันสามารถตั้งค่าวิธีการให้เป็นแบบสาธารณะได้ดังนั้นจึงสามารถทดสอบได้โดยตรง ในทำนองเดียวกันคลาสที่ได้รับจะแสดงตัวแปรอินสแตนซ์ที่เรียกว่า 'ผลลัพธ์' ที่ไม่เคยเปิดเผยมาก่อน

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

ในการทดสอบหน่วยของฉันฉันมีกรณีทดสอบที่สร้างอินสแตนซ์คลาส MockISIQuery และทดสอบเมธอด build_year_range () โดยตรง


2

ในการทดสอบ :: กรอบหน่วยสามารถเขียน

MyClass.send(:public, :method_name)

ที่นี่ "method_name" เป็นวิธีส่วนตัว

& ในขณะที่เรียกวิธีนี้สามารถเขียน

assert_equal expected, MyClass.instance.method_name(params)

1

นี่คือส่วนเพิ่มเติมทั่วไปของคลาสที่ฉันใช้ มันเป็นปืนลูกซองที่ดีกว่าการเปิดเผยวิธีการที่คุณกำลังทดสอบ แต่ในกรณีส่วนใหญ่มันไม่สำคัญและอ่านได้ง่ายกว่ามาก

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

ใช้ส่งเพื่อป้องกันการเข้าถึงวิธีการ / ส่วนตัวจะหักใน 1.9 จึงไม่ได้เป็นทางออกที่แนะนำ


1

เพื่อแก้ไขคำตอบด้านบน: ใน Ruby 1.9.1 เป็น Object # send ที่ส่งข้อความทั้งหมดและ Object # public_send ที่เคารพความเป็นส่วนตัว


1
คุณควรเพิ่มความคิดเห็นในคำตอบนั้นไม่ใช่เขียนคำตอบใหม่เพื่อแก้ไขคำตอบอื่น
zishe

1

แทนที่จะใช้ obj.send คุณสามารถใช้วิธีการแบบซิงเกิลตัน เป็นโค้ดอีก 3 บรรทัดในคลาสทดสอบของคุณและไม่ต้องมีการเปลี่ยนแปลงในโค้ดจริงที่จะทดสอบ

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

ในกรณีการทดสอบที่คุณแล้วใช้เมื่อใดก็ตามที่คุณต้องการที่จะทดสอบmy_private_method_publiclymy_private_method

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

obj.sendสำหรับวิธีส่วนตัวถูกแทนที่ด้วยsend!ใน 1.9 แต่ต่อมาsend!ก็ถูกลบออกอีกครั้ง จึงobj.sendทำงานได้ดีอย่างสมบูรณ์


1

ฉันรู้ว่าฉันไปปาร์ตี้ช้า แต่อย่าทดสอบวิธีส่วนตัว .... ฉันนึกเหตุผลไม่ออกว่าจะทำแบบนี้ วิธีการที่เข้าถึงได้โดยสาธารณะคือการใช้วิธีส่วนตัวที่ใดที่หนึ่งทดสอบวิธีการสาธารณะและสถานการณ์ต่างๆที่อาจทำให้วิธีการส่วนตัวนั้นถูกใช้ มีบางอย่างเข้าบางอย่างออกมา การทดสอบเมธอดส่วนตัวถือเป็นเรื่องที่ไม่ควรทำมากนักและทำให้ยากต่อการรีแฟคเตอร์โค้ดของคุณในภายหลัง เป็นส่วนตัวด้วยเหตุผล


14
ยังไม่เข้าใจตำแหน่งนี้: ใช่วิธีการส่วนตัวเป็นส่วนตัวด้วยเหตุผล แต่ไม่เหตุผลนี้ไม่เกี่ยวข้องกับการทดสอบ
Sebastian อาเจียน Meer

ฉันหวังว่าฉันจะโหวตให้มากกว่านี้ คำตอบเดียวที่ถูกต้องในชุดข้อความนี้
Psynix

หากคุณมีมุมมองเช่นนั้นทำไมถึงต้องกังวลกับการทดสอบหน่วย? เพียงแค่เขียนข้อมูลจำเพาะคุณสมบัติ: อินพุตเข้าไป, หน้าออกมาทุกอย่างที่อยู่ระหว่างนั้นควรจะครอบคลุมใช่ไหม?
ohhh

1

ในการดำเนินการนี้:

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

คุณสามารถใช้สิ่งนี้ในไฟล์ test_helper ของคุณ:

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

ฉันสนุกกับสิ่งนี้: gist.github.com/amomchilov/ef1c84325fe6bb4ce01e0f0780837a82เปลี่ยนชื่อDisrespectเป็นPrivacyViolator(: P) และทำให้disrespect_privacyเมธอดแก้ไขการผูกของบล็อกชั่วคราวเพื่อเตือนวัตถุเป้าหมายไปยังวัตถุ wrapper แต่ในช่วงเวลาดังกล่าวเท่านั้น ของบล็อก ด้วยวิธีนี้คุณไม่จำเป็นต้องใช้พารามิเตอร์บล็อกคุณสามารถอ้างอิงวัตถุด้วยชื่อเดียวกันต่อไป
Alexander - คืนสถานะ Monica
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.