2009-10-09 5 views
149

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

ответ

21

Я нашел лучшее решение в RSpec странице. По-видимому, он поддерживает общие группы примеров. От https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples!

Shared Пример Группа

Вы можете создать общие примеры группы и включают те группы, в другие группы.

Предположим, что у вас есть поведение, которое распространяется на все выпуски вашего продукта, большого и малого.

Во-первых, фактор из «разделенного» поведение:

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

ссылка заблокирована :) –

+0

Обновлено ссылка: https://www.relishapp.com/rspec/rspec-core/v/2-11/docs/example-groups/shared-examples – Jared

20

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

EDIT: Если, как указано в комментариях, модуль ожидает, что некоторые элементы поведения будут присутствовать в классе, в который он смешан, тогда я попытаюсь реализовать манекены этих типов поведения. Этого достаточно, чтобы сделать модуль счастливым выполнять свои обязанности.

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

+0

Что делать, если мой модуль зависит от класса, имеющих определенные атрибуты и поведение? – Andrius

104

Какой майк сказал. Вот простой пример:

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

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

Вот что я имел в виду. Lovely, thanks, +1 –

+3

Любая причина, по которой вы не включили Say' внутри декларации DummyClass вместо вызова 'extend'? –

+2

grant-birchmeier, он 'распространяется на экземпляр класса, т. Е. После вызова' new'. Если вы делали это до вызова 'new', тогда вы правы, вы бы использовали' include' – Hedgehog

172

РАУ путь =>

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

В качестве альтернативы вы можете расширить класс тест с модулем:

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

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

When to use RSpec let()?

+1

Nice. Это помогло мне избежать всех проблем с классом ivars, охватывающим тесты. Давал имена классов, присваивая константам. – captainpete

+0

«cooler», что означает @ gri0n: это 'let' лучше, чем присвоение переменной экземпляра в качестве фиктивного класса в' before (: each) '(или лучше' before (: all) '). ИМО, лучшая причина в том, что вы получите 'NameError', а не' nil', если вы толкните его пальцем. Взгляните на [this SO on, когда использовать let] (http://stackoverflow.com/questions/5359558/when-to-use-rspec-let) – SooDesuNe

+0

Мне это нравится. Он работает для методов, определенных в модуле. Но в одном из моих модулей есть несколько методов, которые действуют на атрибуты класса. Я попытался добавить в свой динамически определенный прокси-класс с attr_accessor, но они не работают в rspec. Как ни странно, они работают в консоли. – pduey

25

Для модулей, которые могут быть проверены в изоляции или насмешливый класс, мне нравится что-то вдоль линий:

модуль:

module MyModule 
    def hallo 
    "hallo" 
    end 
end 

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

describe MyModule do 
    include MyModule 

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

Может показаться неправильным захватить вложенные группы примеров, но мне нравится сжатие. Есть предположения?

+0

Мне это нравится, это так просто. – iain

+1

Может ли это испортить rspec. Я думаю, что лучше использовать метод 'let', описанный @metakungfu. – Automatico

+0

@ Cort3z Вам обязательно нужно убедиться, что имена методов не сталкиваются. Я использую этот подход только тогда, когда все действительно просто. –

9

Принятый ответ - правильный ответ Я думаю, однако я хотел добавить пример использования методов 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 
7

насчет:

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

Я предположил бы, что для больших и много используемых модулей, следует выбрать «Общие группы примеров», как и предлагает ted by @Andrius here. Для простых вещей, для которых вы не хотите беспокоиться о наличии нескольких файлов и т. Д., Вот как обеспечить максимальный контроль над видимостью вашего фиктивного материала (проверенный с помощью 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 
+0

По какой-то причине работает только 'subject {dummy_class.new }'. Случай с 'subject {dummy_class}' не работает для меня. – valk

5

мою недавнюю работу, используя как мало жесткого провода как можно

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 :: Ядро 3.3.0)

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

Очевидно described_class не видно в этой области.

Смежные вопросы