Các mô-đun thử nghiệm trong rspec


175

Các thực hành tốt nhất về các mô-đun thử nghiệm trong rspec là gì? Tôi có một số mô-đun được bao gồm trong một số mô hình và bây giờ tôi chỉ đơn giản là có các thử nghiệm trùng lặp cho từng mô hình (với một vài khác biệt). Có cách nào để DRY nó lên?

Câu trả lời:


219

Cách rad =>

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

Ngoài ra, bạn có thể mở rộng lớp thử nghiệm với mô-đun của mình:

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

Sử dụng 'let' tốt hơn so với sử dụng một biến đối tượng để xác định lớp giả trong trước (: mỗi)

Khi nào nên sử dụng RSpec let ()?


1
Đẹp. Điều này giúp tôi tránh được tất cả các loại vấn đề với các bài kiểm tra kéo dài lớp ngà. Đã cho tên lớp bằng cách gán cho hằng.
captainpete

3
@lulalala Không, đó là một siêu hạng: ruby-doc.org/core-2.0.0/Class.html#method-c-new Để kiểm tra các mô-đun làm một cái gì đó như thế này:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo

26
Cách rad. Tôi thường làm : let(:class_instance) { (Class.new { include Super::Duper::Module }).new }, bằng cách đó, tôi nhận được biến đối tượng thường được sử dụng để kiểm tra bất kỳ cách nào.
Automatico

3
sử dụng includekhông hiệu quả với tôi nhưng extendkhônglet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W

8
Ngay cả radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne

108

Mike nói gì. Đây là một ví dụ tầm thường:

mã mô-đun ...

module Say
  def hello
    "hello"
  end
end

thông số kỹ thuật ...

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
Bất kỳ lý do nào bạn đã không include Saybên trong tuyên bố DummyClass thay vì gọi extend?
Grant Birchmeier

2
Grant-birchmeier, anh ta extendtham gia vào trường hợp của lớp, tức là sau khi newđược gọi. Nếu bạn đang làm điều này trước khi newđược gọi thì bạn đã đúng, bạn sẽ sử dụnginclude
Hedgekey

8
Tôi chỉnh sửa mã để ngắn gọn hơn. @dummy_group = Class.new {extend Say} là tất cả những gì bạn cần để kiểm tra một mô-đun. Tôi nghi ngờ mọi người sẽ thích điều đó vì các nhà phát triển của chúng tôi thường không thích gõ nhiều hơn mức cần thiết.
Tim Harper

@TimHarper Đã thử nhưng các phương thức cá thể đã trở thành các phương thức lớp. Suy nghĩ?
lulalala

6
Tại sao bạn sẽ xác định DummyClasshằng số? Tại sao không chỉ @dummy_class = Class.new? Bây giờ bạn gây ô nhiễm môi trường thử nghiệm của bạn với một định nghĩa lớp không cần thiết. DummyClass này được định nghĩa cho mọi thông số kỹ thuật của bạn và trong thông số kỹ thuật tiếp theo nơi bạn quyết định sử dụng cùng một cách tiếp cận và mở lại định nghĩa DummyClass, nó có thể chứa một cái gì đó (mặc dù trong ví dụ tầm thường này, định nghĩa này hoàn toàn trống rỗng, trong cuộc sống thực trường hợp sử dụng có khả năng một cái gì đó được thêm vào một lúc nào đó và sau đó phương pháp này trở nên nguy hiểm.)
Timo

29

Đối với các mô-đun có thể được kiểm tra độc lập hoặc bằng cách chế nhạo lớp, tôi thích một cái gì đó dọc theo dòng:

mô-đun:

module MyModule
  def hallo
    "hallo"
  end
end

thông số kỹ thuật:

describe MyModule do
  include MyModule

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

Có vẻ sai khi chiếm quyền điều khiển các nhóm ví dụ lồng nhau, nhưng tôi thích sự căng thẳng. Có suy nghĩ gì không?


1
Tôi thích điều này, nó rất đơn giản.
iain

2
Có thể làm rối tung rspec. Tôi nghĩ rằng sử dụng letphương pháp được mô tả bởi @metakungfu là tốt hơn.
Automatico

@ Cort3z Bạn chắc chắn cần đảm bảo rằng các tên phương thức không va chạm. Tôi chỉ sử dụng phương pháp này khi mọi thứ thực sự đơn giản.
Frank C. Schuetz

Điều này làm rối bộ thử nghiệm của tôi do va chạm tên.
roxxypoxxy

24

Tôi tìm thấy một giải pháp tốt hơn trong trang chủ rspec. Rõ ràng nó hỗ trợ các nhóm ví dụ được chia sẻ. Từ https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/spl-examples !

Nhóm ví dụ được chia sẻ

Bạn có thể tạo các nhóm ví dụ được chia sẻ và đưa các nhóm đó vào các nhóm khác.

Giả sử bạn có một số hành vi áp dụng cho tất cả các phiên bản sản phẩm của bạn, cả lớn và nhỏ.

Đầu tiên, yếu tố ra hành vi chia sẻ trên mạng:

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

sau đó khi bạn cần xác định hành vi cho các phiên bản Lớn và Nhỏ, hãy tham chiếu hành vi được chia sẻ bằng phương thức it_should_behave_like ().

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


21

Ngoài đỉnh đầu của tôi, bạn có thể tạo một lớp giả trong tập lệnh thử nghiệm của bạn và đưa mô-đun vào đó không? Sau đó kiểm tra xem lớp giả có hành vi theo cách bạn mong đợi không.

EDIT: Nếu, như đã chỉ ra trong các bình luận, mô-đun mong đợi một số hành vi sẽ có mặt trong lớp mà nó bị trộn lẫn, thì tôi sẽ cố gắng thực hiện các hành vi giả của những hành vi đó. Chỉ cần đủ để làm cho các mô-đun hạnh phúc để thực hiện nhiệm vụ của mình.

Điều đó nói rằng, tôi sẽ hơi lo lắng về thiết kế của mình khi một mô-đun mong đợi rất nhiều từ lớp máy chủ của nó (chúng ta có nói là "máy chủ" không?) - Nếu tôi không được thừa kế từ một lớp cơ sở hoặc không thể tiêm chức năng mới vào cây thừa kế sau đó tôi nghĩ rằng tôi đang cố gắng giảm thiểu bất kỳ kỳ vọng nào mà mô-đun có thể có. Mối quan tâm của tôi là thiết kế của tôi sẽ bắt đầu phát triển một số lĩnh vực không linh hoạt khó chịu.


Điều gì xảy ra nếu mô-đun của tôi phụ thuộc vào lớp có các thuộc tính và hành vi nhất định?
Andrius

10

Câu trả lời được chấp nhận là câu trả lời đúng tôi nghĩ, tuy nhiên tôi muốn thêm một ví dụ về cách sử dụng rpsecs shared_examples_forit_behaves_likephương thức. Tôi đề cập đến một vài thủ thuật trong đoạn mã nhưng để biết thêm thông tin, hãy xem hướng dẫn relishapp-rspec này .

Với điều này, bạn có thể kiểm tra mô-đun của mình trong bất kỳ lớp nào bao gồm nó. Vì vậy, bạn thực sự đang thử nghiệm những gì bạn sử dụng trong ứng dụng của bạn.

Hãy xem một ví dụ:

# 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

Bây giờ hãy tạo spec cho mô-đun của chúng tôi: 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

Thế còn:

describe MyModule do
  subject { Object.new.extend(MyModule) }
  it "does stuff" do
    expect(subject.does_stuff?).to be_true
  end
end

6

Tôi sẽ đề nghị rằng đối với các mô-đun lớn hơn và được sử dụng nhiều, bạn nên chọn "Nhóm ví dụ được chia sẻ" theo đề xuất của @Andrius tại đây . Đối với những thứ đơn giản mà bạn không muốn gặp phải sự cố khi có nhiều tệp, v.v ... đây là cách đảm bảo kiểm soát tối đa khả năng hiển thị của công cụ giả của bạn (đã thử nghiệm với rspec 2.14.6, chỉ cần sao chép và dán mã vào spec file và chạy nó):

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

Đối với một số lý do chỉ subject { dummy_class.new }là làm việc. Trường hợp subject { dummy_class }không làm việc cho tôi.
valk

6

công việc gần đây của tôi, sử dụng càng ít dây càng tốt

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

Tôi ước

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

đã hoạt động nhưng không được (như tại Ruby MRI 2.2.3 và 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>

Rõ ràng mô tả_ class không thể nhìn thấy trong phạm vi đó.


5

Để kiểm tra mô-đun của bạn, sử dụng:

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

  # examples
end

Để DRY up một số thứ bạn sử dụng trên nhiều thông số kỹ thuật, bạn có thể sử dụng bối cảnh được chia sẻ:

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

Tài nguyên:



0

bạn chỉ cần bao gồm mô-đun của bạn mudule Test module MyModule def test 'test' end end end vào tệp spec của bạn trong tệp spec của bạn 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

Một giải pháp khả thi để thử nghiệm phương thức mô-đun độc lập trên lớp sẽ bao gồm chúng

module moduleToTest
  def method_to_test
    'value'
  end
end

Và thông số kỹ thuật cho nó

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

Và nếu bạn muốn DRY kiểm tra chúng, thì shared_examples là cách tiếp cận tốt


Tôi không phải là người đánh giá thấp bạn, nhưng tôi khuyên bạn nên thay thế hai LET của bạn bằng subject(:module_to_test_instance) { Class.new.include(described_class) }. Nếu không, tôi không thực sự thấy bất cứ điều gì sai với câu trả lời của bạn.
Allison

-1

Đây là một mẫu lặp lại vì bạn sẽ cần kiểm tra nhiều hơn một mô-đun. Vì lý do đó, điều này là hơn cả mong muốn để tạo ra một người trợ giúp cho việc này.

Tôi tìm thấy bài đăng này giải thích cách thực hiện nhưng tôi đang đối phó ở đây vì trang web có thể bị gỡ xuống tại một số điểm.

Điều này là để tránh các thể hiện đối tượng không thực hiện phương thức cá thể :: bất kỳ lỗi nào bạn gặp phải khi thử allowcác phương thức trên dummylớp.

Mã số:

Trong 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

Trong 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

Trong thông số kỹ thuật của bạn:

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