Тестирование модулей в rspec

175

Каковы лучшие практики тестирования модулей в rspec? У меня есть несколько модулей, которые включены в несколько моделей, и сейчас у меня просто есть дублирующие тесты для каждой модели (с небольшими отличиями). Есть ли способ высушить это?

Андрюс
источник

Ответы:

219

Рад пути =>

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

В качестве альтернативы вы можете расширить тестовый класс с помощью вашего модуля:

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

Использование 'let' лучше, чем использование переменной экземпляра для определения фиктивного класса в before (: each)

Когда использовать RSpec let ()?

metakungfu
источник
1
Ницца. Это помогло мне избежать всевозможных проблем с тестами spanning класса 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 }таким образом я получаю переменную экземпляра, которая чаще всего используется для тестирования любым способом.
Автомат
3
использование includeне работает для меня, но extendделаетlet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Майк 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
Грант-Берчмайер, он входит extendв экземпляр класса, то есть после того, newкак был вызван. Если бы вы делали это до того, newкак вас вызвали, значит, вы правы, вы бы использовалиinclude
Ежик
8
Я отредактировал код, чтобы быть более кратким. @dummy_class = Class.new {extend Say} - все, что вам нужно для тестирования модуля. Я подозреваю, что люди предпочтут это, поскольку мы, разработчики, часто не любим печатать больше, чем необходимо.
Тим Харпер
@TimHarper Пробовал, но методы экземпляра стали методами класса. Мысли?
Лулалала
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
Мне это нравится, это так просто.
Иэн
2
Может испортить rspec. Я думаю, что letлучше использовать метод, описанный @metakungfu.
Автомат
@ 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
Андрюс
источник
Обновленная ссылка: relishapp.com/rspec/rspec-core/v/2-11/docs/example-groups/…
Джаред
21

Сверх того, не могли бы вы создать в своем тестовом скрипте фиктивный класс и включить в него модуль? Затем проверьте, что фиктивный класс ведет себя так, как вы ожидаете.

РЕДАКТИРОВАТЬ: Если, как указано в комментариях, модуль ожидает, что некоторые поведения будут присутствовать в классе, в который он смешан, то я бы попытался реализовать макеты этих поведений. Достаточно, чтобы модуль с радостью выполнял свои обязанности.

Тем не менее, я буду немного нервничать по поводу своего дизайна, когда модуль ожидает многого от своего хоста (мы говорим «хост»?) Класса - если я еще не наследую от базового класса или не могу внедрить новая функциональность в дереве наследования, то я думаю, что я буду пытаться минимизировать любые такие ожидания, которые может иметь модуль. Меня беспокоит то, что мой дизайн начнет развиваться в некоторых областях с неприятной негибкостью.

Майк Вудхаус
источник
Что если мой модуль зависит от класса, имеющего определенные атрибуты и поведение?
Андрюс
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
p1100i
источник
6

Что о:

describe MyModule do
  subject { Object.new.extend(MyModule) }
  it "does stuff" do
    expect(subject.does_stuff?).to be_true
  end
end
Мэтт Коннолли
источник
6

Я хотел бы предложить, чтобы для более крупных и часто используемых модулей был выбран «Shared Example Groups», как предложено здесь @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
Timo
источник
По какой-то причине только subject { dummy_class.new }работает. Случай с subject { dummy_class }не работает для меня.
Валк
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>

Очевидно, описанный_класс не виден в этой области.

Лейф
источник
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

Ресурсы:

Allison
источник
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

mdlx
источник
-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 их протестировать, то shared_examples - хороший подход.

Nermin
источник
Я не был тем, кто отказал тебе в голосовании, но я предлагаю заменить твои два LET на 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
juliangonzalez
источник