การทดสอบโมดูลใน rspec


175

อะไรคือแนวปฏิบัติที่ดีที่สุดสำหรับโมดูลการทดสอบใน rspec ฉันมีโมดูลบางตัวที่รวมอยู่ในรุ่นไม่กี่รุ่นและตอนนี้ฉันมีการทดสอบซ้ำกันสำหรับแต่ละรุ่น (มีความแตกต่างเล็กน้อย) มีวิธีที่จะทำให้มันแห้งหรือไม่?

คำตอบ:


219

วิธี rad =>

let(:dummy_class) { Class.new { include ModuleToBeTested } }

หรือคุณสามารถขยายคลาสการทดสอบด้วยโมดูลของคุณ:

let(:dummy_class) { Class.new { extend ModuleToBeTested } }

การใช้ 'ให้' ดีกว่าการใช้ตัวแปรอินสแตนซ์เพื่อกำหนดระดับหุ่นจำลองในก่อนหน้า (: แต่ละรายการ)

เมื่อใดที่จะใช้ RSpec let ()?


1
ดี สิ่งนี้ช่วยให้ฉันหลีกเลี่ยงปัญหาทุกประเภทด้วยการทดสอบในระดับ ivars ซึ่งขยายออกไป สร้างชื่อคลาสโดยกำหนดให้กับค่าคงที่
captainpete

3
@lulalala ไม่มีก็ชั้นยอด: ruby-doc.org/core-2.0.0/Class.html#method-c-newโมดูลการทดสอบให้ทำอะไรเช่นนี้let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo

26
วิธี rad ฉันมักจะ: let(:class_instance) { (Class.new { include Super::Duper::Module }).new }วิธีที่ฉันได้รับตัวแปรอินสแตนซ์ที่ใช้บ่อยที่สุดสำหรับการทดสอบ แต่อย่างใด
Automatico

3
การใช้งานincludeไม่ได้ผลสำหรับฉัน แต่extendจะทำได้let(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W

8
แม้แต่เรเดอร์:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne

108

ไมค์พูดอะไร นี่เป็นตัวอย่างเล็กน้อย:

รหัสโมดูล ...

module Say
  def hello
    "hello"
  end
end

ชิ้นส่วน spec ...

class DummyClass
end

before(:each) do
  @dummy_class = DummyClass.new
  @dummy_class.extend(Say)
end

it "get hello string" do
  expect(@dummy_class.hello).to eq "hello"
end

3
มีเหตุผลใดที่คุณไม่ได้include Sayอยู่ในประกาศของ DummyClass แทนที่จะโทรมาextend?
สิทธิ์ Birchmeier

2
แกรนท์ - เบิร์ชไมเออร์เขาextendเข้าสู่กลุ่มตัวอย่างเช่นหลังจากnewถูกเรียก หากคุณเคยทำสิ่งนี้มาก่อนจะnewมีการเรียกตัวคุณถูกต้องคุณจะใช้include
Hedgehog

8
ฉันแก้ไขรหัสให้กระชับยิ่งขึ้น @dummy_class = Class.new {ขยาย Say} คือทั้งหมดที่คุณต้องทดสอบโมดูล ฉันสงสัยว่าคนจะชอบมากกว่านี้เพราะนักพัฒนาของเรามักไม่ชอบพิมพ์มากกว่าที่จำเป็น
Tim Harper

@TimHarper พยายาม แต่วิธีการอินสแตนซ์กลายเป็นวิธีการเรียน คิด?
lulalala

6
ทำไมคุณต้องกำหนดDummyClassค่าคงที่ ทำไมไม่เป็นเช่นนั้น@dummy_class = Class.new? ตอนนี้คุณทำให้สภาพแวดล้อมการทดสอบของคุณสกปรกด้วยการกำหนดคลาสที่ไม่จำเป็น DummyClass นี้ถูกกำหนดไว้สำหรับสเปคของคุณทุกคนและในสเปคถัดไปที่คุณตัดสินใจที่จะใช้แนวทางเดียวกันและเปิดนิยาม DummyClass อีกครั้งมันอาจมีบางสิ่งอยู่แล้ว (แม้ว่าในตัวอย่างเล็ก ๆ น้อย ๆ นี้ กรณีการใช้งานอาจเป็นไปได้ว่ามีบางอย่างเพิ่มเข้ามาในบางจุดและวิธีการนี้จะกลายเป็นอันตราย)
Timo

29

สำหรับโมดูลที่สามารถทดสอบในการแยกหรือโดยการเยาะเย้ยชั้นฉันชอบบางสิ่งตามแนวของ:

โมดูล:

module MyModule
  def hallo
    "hallo"
  end
end

ข้อมูลจำเพาะ:

describe MyModule do
  include MyModule

  it { hallo.should == "hallo" }
end

มันอาจจะผิดที่จะจี้กลุ่มตัวอย่างที่ซ้อนกัน แต่ฉันชอบความตึงเครียด ความคิดใด ๆ


1
ฉันชอบสิ่งนี้มันตรงไปตรงมามาก
Iain

2
อาจเลอะ rspec ฉันคิดว่าการใช้letวิธีที่อธิบายโดย @metakungfu นั้นดีกว่า
Automatico

@ Cort3z แน่นอนคุณต้องแน่ใจว่าชื่อวิธีการไม่ชนกัน ฉันใช้วิธีการนี้เฉพาะเมื่อสิ่งต่าง ๆ เรียบง่ายจริงๆ
Frank C. Schuetz

สิ่งนี้ทำให้ชุดทดสอบของฉันยุ่งเพราะมีการปะทะกันของชื่อ
roxxypoxxy

24

ฉันพบทางออกที่ดีกว่าในหน้าแรกของ rspec เห็นได้ชัดว่ามันรองรับกลุ่มตัวอย่างที่แชร์ จากhttps://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

กลุ่มตัวอย่างที่ใช้ร่วมกัน

คุณสามารถสร้างกลุ่มตัวอย่างที่ใช้ร่วมกันและรวมกลุ่มเหล่านั้นไว้ในกลุ่มอื่น

สมมติว่าคุณมีพฤติกรรมบางอย่างที่ใช้กับผลิตภัณฑ์ทุกรุ่นของคุณทั้งขนาดใหญ่และขนาดเล็ก

ก่อนอื่นให้คำนึงถึงพฤติกรรมที่ "แบ่งปัน":

shared_examples_for "all editions" do   
  it "should behave like all editions" do   
  end 
end

จากนั้นเมื่อคุณต้องการกำหนดพฤติกรรมสำหรับรุ่นขนาดใหญ่และขนาดเล็กให้อ้างอิงพฤติกรรมที่ใช้ร่วมกันโดยใช้เมธอด it_should_behave_like ()

describe "SmallEdition" do  
  it_should_behave_like "all editions"
  it "should also behave like a small edition" do   
  end 
end

ลิงก์ที่อัปเดต: relishapp.com/rspec/rspec-core/v/2-11/docs/example-groups/…
Jared

21

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

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

ที่กล่าวว่าฉันจะกังวลเล็กน้อยเกี่ยวกับการออกแบบของฉันเมื่อโมดูลคาดหวังมากจากโฮสต์ (เราจะพูดว่า "host"?) class - ถ้าฉันไม่ได้รับมรดกจากชั้นฐานหรือไม่สามารถฉีด ฟังก์ชั่นใหม่ในแผนผังการสืบทอดจากนั้นฉันคิดว่าฉันจะพยายามลดความคาดหวังใด ๆ ที่โมดูลอาจมี ความกังวลของฉันคือการที่การออกแบบของฉันจะเริ่มพัฒนาบางส่วนของความยืดหยุ่นที่ไม่พึงประสงค์


ถ้าโมดูลของฉันขึ้นอยู่กับคลาสที่มีคุณลักษณะและพฤติกรรมที่แน่นอน
Andrius

10

คำตอบที่ยอมรับคือคำตอบที่ถูกต้องฉันคิดว่าฉันต้องการเพิ่มตัวอย่างวิธีใช้ rpsecs shared_examples_forและit_behaves_likeวิธีการ ฉันพูดถึงเทคนิคเล็กน้อยในข้อมูลโค้ด แต่สำหรับข้อมูลเพิ่มเติมดูคู่มือ relishapp-rspec-guideนี้

ด้วยวิธีนี้คุณสามารถทดสอบโมดูลของคุณในชั้นเรียนใด ๆ ที่รวมอยู่ด้วย ดังนั้นคุณกำลังทดสอบสิ่งที่คุณใช้ในแอปพลิเคชันของคุณจริงๆ

ลองดูตัวอย่าง:

# Lets assume a Movable module
module Movable
  def self.movable_class?
    true
  end

  def has_feets?
    true
  end
end

# Include Movable into Person and Animal
class Person < ActiveRecord::Base
  include Movable
end

class Animal < ActiveRecord::Base
  include Movable
end

ตอนนี้ให้สร้างข้อกำหนดสำหรับโมดูลของเรา: movable_spec.rb

shared_examples_for Movable do
  context 'with an instance' do
    before(:each) do
      # described_class points on the class, if you need an instance of it: 
      @obj = described_class.new

      # or you can use a parameter see below Animal test
      @obj = obj if obj.present?
    end

    it 'should have feets' do
      @obj.has_feets?.should be_true
    end
  end

  context 'class methods' do
    it 'should be a movable class' do
      described_class.movable_class?.should be_true
    end
  end
end

# Now list every model in your app to test them properly

describe Person do
  it_behaves_like Movable
end

describe Animal do
  it_behaves_like Movable do
    let(:obj) { Animal.new({ :name => 'capybara' }) }
  end
end


6

ผมขอแนะนำว่าสำหรับขนาดใหญ่และโมดูลใช้มากควรเลือกหนึ่งสำหรับ "ที่ใช้ร่วมกันตัวอย่างกลุ่ม" ตามที่แนะนำโดย @Andrius ที่นี่ สำหรับสิ่งง่าย ๆ ที่คุณไม่ต้องการให้มีปัญหาในการมีไฟล์หลาย ๆ ไฟล์เป็นต้นนี่คือวิธีการควบคุมการมองเห็นสิ่งจำลองของคุณได้อย่างเต็มที่ (ทดสอบด้วย rspec 2.14.6 เพียงคัดลอกและวางรหัสลงใน ไฟล์ spec และเรียกใช้):

module YourCoolModule
  def your_cool_module_method
  end
end

describe YourCoolModule do
  context "cntxt1" do
    let(:dummy_class) do
      Class.new do
        include YourCoolModule

        #Say, how your module works might depend on the return value of to_s for
        #the extending instances and you want to test this. You could of course
        #just mock/stub, but since you so conveniently have the class def here
        #you might be tempted to use it?
        def to_s
          "dummy"
        end

        #In case your module would happen to depend on the class having a name
        #you can simulate that behaviour easily.
        def self.name
          "DummyClass"
        end
      end
    end

    context "instances" do
      subject { dummy_class.new }

      it { subject.should be_an_instance_of(dummy_class) }
      it { should respond_to(:your_cool_module_method)}
      it { should be_a(YourCoolModule) }
      its (:to_s) { should eq("dummy") }
    end

    context "classes" do
      subject { dummy_class }
      it { should be_an_instance_of(Class) }
      it { defined?(DummyClass).should be_nil }
      its (:name) { should eq("DummyClass") }
    end
  end

  context "cntxt2" do
    it "should not be possible to access let methods from anohter context" do
      defined?(dummy_class).should be_nil
    end
  end

  it "should not be possible to access let methods from a child context" do
    defined?(dummy_class).should be_nil
  end
end

#You could also try to benefit from implicit subject using the descbie
#method in conjunction with local variables. You may want to scope your local
#variables. You can't use context here, because that can only be done inside
#a describe block, however you can use Porc.new and call it immediately or a
#describe blocks inside a describe block.

#Proc.new do
describe "YourCoolModule" do #But you mustn't refer to the module by the
  #constant itself, because if you do, it seems you can't reset what your
  #describing in inner scopes, so don't forget the quotes.
  dummy_class = Class.new { include YourCoolModule }
  #Now we can benefit from the implicit subject (being an instance of the
  #class whenever we are describing a class) and just..
  describe dummy_class do
    it { should respond_to(:your_cool_module_method) }
    it { should_not be_an_instance_of(Class) }
    it { should be_an_instance_of(dummy_class) }
    it { should be_a(YourCoolModule) }
  end
  describe Object do
    it { should_not respond_to(:your_cool_module_method) }
    it { should_not be_an_instance_of(Class) }
    it { should_not be_an_instance_of(dummy_class) }
    it { should be_an_instance_of(Object) }
    it { should_not be_a(YourCoolModule) }
  end
#end.call
end

#In this simple case there's necessarily no need for a variable at all..
describe Class.new { include YourCoolModule } do
  it { should respond_to(:your_cool_module_method) }
  it { should_not be_a(Class) }
  it { should be_a(YourCoolModule) }
end

describe "dummy_class not defined" do
  it { defined?(dummy_class).should be_nil }
end

ด้วยเหตุผลบางอย่างเท่านั้นที่subject { dummy_class.new }ทำงาน กรณีที่ใช้subject { dummy_class }ไม่ได้กับฉัน
valk

6

งานล่าสุดของฉันโดยใช้การเดินสายแบบแรงเล็กน้อยเท่าที่จะทำได้

require 'spec_helper'

describe Module::UnderTest do
  subject {Object.new.extend(described_class)}

  context '.module_method' do
    it {is_expected.to respond_to(:module_method)}
    # etc etc
  end
end

ฉันหวังว่า

subject {Class.new{include described_class}.new}

ใช้งานได้ แต่ใช้ไม่ได้ (เช่น Ruby MRI 2.2.3 และ RSpec :: Core 3.3.0)

Failure/Error: subject {Class.new{include described_class}.new}
  NameError:
    undefined local variable or method `described_class' for #<Class:0x000000063a6708>

ชัดเจน __class จะไม่ปรากฏในขอบเขตนั้น


6

เพื่อทดสอบโมดูลของคุณใช้:

describe MyCoolModule do
  subject(:my_instance) { Class.new.extend(described_class) }

  # examples
end

ในการ DRY บางสิ่งที่คุณใช้กับสเปคหลาย ๆ ตัวคุณสามารถใช้บริบทที่แชร์:

RSpec.shared_context 'some shared context' do
  let(:reused_thing)       { create :the_thing }
  let(:reused_other_thing) { create :the_thing }

  shared_examples_for 'the stuff' do
    it { ... }
    it { ... }
  end
end
require 'some_shared_context'

describe MyCoolClass do
  include_context 'some shared context'

  it_behaves_like 'the stuff'

  it_behaves_like 'the stuff' do
    let(:reused_thing) { create :overrides_the_thing_in_shared_context }
  end
end

แหล่งข้อมูล:



0

คุณต้องรวมโมดูลของคุณไปยังไฟล์ mudule Test module MyModule def test 'test' end end end ข้อมูลจำเพาะของคุณในไฟล์ข้อมูลจำเพาะของคุณ RSpec.describe Test::MyModule do include Test::MyModule #you can call directly the method *test* it 'returns test' do expect(test).to eql('test') end end


-1

ทางออกหนึ่งที่เป็นไปได้สำหรับวิธีการทดสอบโมดูลซึ่งเป็นอิสระในชั้นเรียนที่จะรวมพวกเขา

module moduleToTest
  def method_to_test
    'value'
  end
end

และสเป็คมัน

describe moduleToTest do
  let(:dummy_class) { Class.new { include moduleToTest } }
  let(:subject) { dummy_class.new }

  describe '#method_to_test' do
    it 'returns value' do
      expect(subject.method_to_test).to eq('value')
    end
  end
end

และหากคุณต้องการทดสอบพวกเขาให้แห้งแล้วshared_examplesเป็นวิธีการที่ดี


ผมไม่ได้เป็นคนหนึ่งที่ downvoted คุณ subject(:module_to_test_instance) { Class.new.include(described_class) }แต่ผมขอแนะนำให้คุณเปลี่ยนสองช่วยให้กับ ไม่อย่างนั้นฉันไม่เห็นอะไรผิดปกติกับคำตอบของคุณ
อัลลิสัน

-1

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

ฉันพบโพสต์นี้ที่อธิบายถึงวิธีการทำ แต่ฉันจัดการที่นี่เนื่องจากเว็บไซต์อาจถูกลบในบางจุด

นี่คือการหลีกเลี่ยงอินสแตนซ์ของวัตถุที่ไม่ได้ใช้วิธีการอินสแตนซ์::ข้อผิดพลาดใด ๆ ที่คุณได้รับเมื่อพยายามใช้allowวิธีการdummyชั้นเรียน

รหัส:

ใน spec/support/helpers/dummy_class_helpers.rb

module DummyClassHelpers

  def dummy_class(name, &block)
    let(name.to_s.underscore) do
      klass = Class.new(&block)

      self.class.const_set name.to_s.classify, klass
    end
  end

end

ใน spec/spec_helper.rb

# skip this if you want to manually require
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}

RSpec.configure do |config|
  config.extend DummyClassHelpers
end

ในรายละเอียดของคุณ:

require 'spec_helper'

RSpec.shared_examples "JsonSerializerConcern" do

  dummy_class(:dummy)

  dummy_class(:dummy_serializer) do
     def self.represent(object)
     end
   end

  describe "#serialize_collection" do
    it "wraps a record in a serializer" do
      expect(dummy_serializer).to receive(:represent).with(an_instance_of(dummy)).exactly(3).times

      subject.serialize_collection [dummy.new, dummy.new, dummy.new]
    end
  end
end
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.