Тестування модулів у rspec


175

Які найкращі практики тестування модулів у rspec? У мене є кілька модулів, які включаються в декілька моделей, і зараз я просто маю дублюючі тести для кожної моделі (з невеликими відмінностями). Чи є спосіб ПУСИТИ його?

Відповіді:


219

Рад шлях =>

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

Крім того, ви можете розширити тестовий клас за допомогою свого модуля:

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

Використовувати "Let" краще, ніж використовувати змінну екземпляра для визначення класу манекена раніше (: кожен)

Коли використовувати RSpec let ()?


1
Приємно. Це допомогло мені уникнути всіляких проблем із тестами, що стосуються класів ivars. Дайте назви класів, призначивши константи.
капітан

3
@lulalala Ні, це супер клас: ruby-doc.org/core-2.0.0/Class.html#method-c-new Для тестування модулів зробіть щось подібне:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Тимо

26
Шлях рад. Я зазвичай роблю: 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 }
Річард-Деген

108

Який Майк сказав. Ось банальний приклад:

код модуля ...

module Say
  def hello
    "hello"
  end
end

фрагмент специфікації ...

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?
Грант Бірхмайер

2
grant-birchmeier, він extendвходить до екземпляра класу, тобто після того, newяк його викликали. Якщо ви робили це раніше, newяк вас називають, ви маєте рацію, ви б скористалисяinclude
Їжак

8
Я відредагував код, щоб бути більш стислим. @dummy_class = Class.new {extension Say} - все, що потрібно для тестування модуля. Я підозрюю, що люди віддадуть перевагу тому, що ми, розробники, часто не любимо набирати більше, ніж потрібно.
Тім Харпер

@TimHarper Пробував, але методи екземпляра стали методами класу. Думки?
lulalala

6
Чому б ви визначали DummyClassконстанту? Чому б не просто @dummy_class = Class.new? Тепер ви забруднюєте тестове середовище непотрібним визначенням класу. Цей DummyClass визначений для кожної вашої специфікації, а в наступній специфікації, де ви вирішили використовувати той самий підхід і повторно відкрити визначення DummyClass, воно може вже містити щось (хоча в цьому тривіальному прикладі визначення суворо порожнє, в реальному житті використовуйте випадки, ймовірно, що-небудь додасться в якийсь момент, і тоді цей підхід стає небезпечним.)
Тимо

29

Для модулів, які можна перевірити ізольовано або знущаючись над класом, мені щось подобається:

модуль:

module MyModule
  def hallo
    "hallo"
  end
end

специфікація:

describe MyModule do
  include MyModule

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

Це може здатися неправильним для викрадення вкладених прикладних груп, але мені подобається лаконічність. Будь-які думки?


1
Мені це подобається, це так просто.
1313

2
Може зіпсувати rspec. Я думаю, що letкраще використовувати метод, описаний @metakungfu.
Automatico

@ Cort3z Вам обов'язково потрібно переконатися, що назви методів не стикаються. Я використовую такий підхід лише тоді, коли речі справді прості.
Френк С. Шует

Це зіпсувало мій тестовий набір через зіткнення імені.
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


21

Чи зможете ви створити фіктивний клас у своєму тестовому сценарії та включити до нього модуль? Потім перевірте, що фіктивний клас має поведінку так, як ви очікували.

EDIT: Якщо, як зазначено в коментарях, модуль очікує, що в класі, в який він змішаний, буде присутній певний тип поведінки, то я б спробував реалізувати манекени цих поведінок. Досить просто, щоб модуль із задоволенням виконував свої обов'язки.

Це сказало, що я буду трохи нервувати свою конструкцію, коли модуль очікує від своєї хостової (чи ми говоримо "хост"?) Класу багато - Якщо я вже не успадкував базовий клас або не можу ввести його нову функціональність у спадковому дереві, то, я думаю, я намагаюся мінімізувати будь-які такі очікування, які може мати модуль. Мене турбує те, що мій дизайн почне розробляти деякі сфери неприємної негнучкості.


Що робити, якщо мій модуль залежить від класу, який має певні атрибути та поведінку?
Андрій

10

Я вважаю, що прийнята відповідь - це правильна відповідь, проте я хотів додати приклад, як використовувати rpsecs shared_examples_forта it_behaves_likeметоди. Я згадую кілька хитрощів у фрагменті коду, але для отримання додаткової інформації дивіться цей посібник з relishapp-rspec .

За допомогою цього ви можете протестувати свій модуль у будь-якому з класів, які його включають. Таким чином, ви дійсно тестуєте те, що використовуєте у своїй програмі.

Давайте подивимось приклад:

# 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, просто скопіюйте та вставте код у файл специфікації та запустіть його:

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

Щоб висушити деякі речі, які ви використовуєте в кількох специфікаціях, ви можете використовувати спільний контекст:

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

І якщо ви хочете DRY перевірити їх, то спільні_приклади - це хороший підхід


Я не був тим, хто звернувся до вас, але я пропоную замінити свої два LETs 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.