Módulos de prueba en rspec

175

¿Cuáles son las mejores prácticas para probar módulos en rspec? Tengo algunos módulos que se incluyen en algunos modelos y por ahora simplemente tengo pruebas duplicadas para cada modelo (con pocas diferencias). ¿Hay alguna manera de SECARLO?

Andrius
fuente

Respuestas:

219

La forma rad =>

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

Alternativamente, puede extender la clase de prueba con su módulo:

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

Usar 'let' es mejor que usar una variable de instancia para definir la clase ficticia en el antes (: cada)

¿Cuándo usar RSpec let ()?

metakungfu
fuente
1
Agradable. Esto me ayudó a evitar todo tipo de problemas con ivars de clase que abarcan pruebas. Dio los nombres de las clases mediante la asignación de constantes.
captainpete
3
@lulalala No, es una superclase : ruby-doc.org/core-2.0.0/Class.html#method-c-new Para probar módulos, haga algo como esto:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo
26
Way rad. Usualmente lo hago: de let(:class_instance) { (Class.new { include Super::Duper::Module }).new }esa manera obtengo la variable de instancia que se usa con mayor frecuencia para probar de cualquier manera.
Automático el
3
usar includeno funciona para mí, pero lo extendhacelet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W
8
Incluso más radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne
108

Lo que dijo Mike. Aquí hay un ejemplo trivial:

código de módulo ...

module Say
  def hello
    "hello"
  end
end

fragmento de especificación ...

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
Karmen Blake
fuente
3
¿Alguna razón por la que no lo hiciste include Saydentro de la declaración DummyClass en lugar de llamar extend?
Grant Birchmeier
2
Grant-Birchmeier, él está extendentrando en la instancia de la clase, es decir, después de newhaber sido llamado. Si hicieras esto antes newse llama, entonces tienes razóninclude
usarías
8
Edité el código para ser más conciso. @dummy_class = Class.new {extender Say} es todo lo que necesita para probar un módulo. Sospecho que la gente preferirá eso ya que a los desarrolladores a menudo no les gusta escribir más de lo necesario.
Tim Harper el
@TimHarper Probado pero los métodos de instancia se convirtieron en métodos de clase. Pensamientos?
lulalala
66
¿Por qué definirías la DummyClassconstante? ¿Por qué no solo @dummy_class = Class.new? Ahora estás contaminando tu entorno de prueba con una definición de clase innecesaria. Este DummyClass se define para todas y cada una de sus especificaciones y en la siguiente especificación donde decide usar el mismo enfoque y volver a abrir la definición de DummyClass, es posible que ya contenga algo (aunque en este ejemplo trivial la definición está estrictamente vacía, en la vida real casos de uso es probable que algo se agregue en algún momento y luego este enfoque se vuelva peligroso.)
Timo
29

Para los módulos que se pueden probar de forma aislada o burlándose de la clase, me gusta algo como:

módulo:

module MyModule
  def hallo
    "hallo"
  end
end

Especificaciones:

describe MyModule do
  include MyModule

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

Puede parecer incorrecto secuestrar grupos de ejemplos anidados, pero me gusta la brevedad. ¿Alguna idea?

Frank C. Schuetz
fuente
1
Me gusta esto, es muy sencillo.
desde
2
Podría estropear el rspec. Creo que usar el letmétodo descrito por @metakungfu es mejor.
Automático el
@ Cort3z Definitivamente debe asegurarse de que los nombres de los métodos no colisionen. Estoy usando este enfoque solo cuando las cosas son realmente simples.
Frank C. Schuetz
Esto estropeó mi conjunto de pruebas debido a la colisión de nombres.
roxxypoxxy
24

Encontré una solución mejor en la página de inicio de rspec. Aparentemente, admite grupos de ejemplo compartidos. ¡Desde https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

Grupos de ejemplo compartidos

Puede crear grupos de ejemplo compartidos e incluir esos grupos en otros grupos.

Suponga que tiene un comportamiento que se aplica a todas las ediciones de su producto, tanto grandes como pequeñas.

Primero, factorice el comportamiento "compartido":

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

luego, cuando necesite definir el comportamiento para las ediciones Grande y Pequeña, haga referencia al comportamiento compartido utilizando el método it_should_behave_like ().

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

Fuera de mi cabeza, ¿podrías crear una clase ficticia en tu script de prueba e incluir el módulo en eso? Luego compruebe que la clase ficticia tiene el comportamiento de la manera que esperaría.

EDITAR: si, como se señala en los comentarios, el módulo espera que algunos comportamientos estén presentes en la clase en la que se mezclan, entonces trataría de implementar dummies de esos comportamientos. Solo lo suficiente para que el módulo esté feliz de realizar sus tareas.

Dicho esto, me pondría un poco nervioso por mi diseño cuando un módulo espera mucho de su clase host (¿decimos "host"?) - Si aún no heredé de una clase base o no puedo inyectar la nueva funcionalidad en el árbol de herencia, entonces creo que estaría tratando de minimizar cualquier expectativa que pueda tener un módulo. Mi preocupación es que mi diseño comenzaría a desarrollar algunas áreas de inflexibilidad desagradable.

Mike Woodhouse
fuente
¿Qué sucede si mi módulo depende de que la clase tenga ciertos atributos y comportamiento?
Andrius
10

Creo que la respuesta aceptada es la respuesta correcta, sin embargo, quería agregar un ejemplo de cómo usar rpsecs shared_examples_fory it_behaves_likemétodos. Menciono algunos trucos en el fragmento de código, pero para obtener más información, consulte esta guía de relishapp-rspec .

Con esto puede probar su módulo en cualquiera de las clases que lo incluyen. Entonces realmente está probando lo que usa en su aplicación.

Veamos un ejemplo:

# 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

Ahora creemos especificaciones para nuestro módulo: 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
fuente
6

Qué pasa:

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

Sugeriría que para módulos más grandes y muy usados ​​uno debería optar por los "Grupos de ejemplo compartidos" como lo sugiere @Andrius aquí . Para cosas simples para las que no desea pasar por la molestia de tener varios archivos, etc., aquí le mostramos cómo garantizar el máximo control sobre la visibilidad de sus cosas ficticias (probado con rspec 2.14.6, simplemente copie y pegue el código en un archivo de especificaciones y ejecutarlo):

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
fuente
Por alguna razón solo subject { dummy_class.new }está funcionando. El caso con subject { dummy_class }no me funciona.
valk
6

mi trabajo reciente, utilizando el menor cableado posible

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

Yo deseo

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

funcionó, pero no funciona (como en Ruby MRI 2.2.3 y 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>

Obviamente, describe_class no es visible en ese ámbito.

Leif
fuente
6

Para probar su módulo, use:

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

  # examples
end

Para SECAR algunas cosas que usa en múltiples especificaciones, puede usar un contexto compartido:

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

Recursos:

Allison
fuente
0

simplemente debe incluir su módulo en su archivo de especificaciones mudule Test module MyModule def test 'test' end end end en su archivo de especificaciones 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
fuente
-1

Una posible solución para probar el método del módulo que es independiente de la clase que los incluirá

module moduleToTest
  def method_to_test
    'value'
  end
end

Y especificaciones para ello

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

Y si quieres probarlos en SECO, entonces shared_examples es un buen enfoque

Nermin
fuente
No fui yo quien te rechazó, pero sugiero reemplazar tus dos LET con subject(:module_to_test_instance) { Class.new.include(described_class) }. De lo contrario, realmente no veo nada malo en tu respuesta.
Allison
-1

Este es un patrón recurrente ya que necesitará probar más de un módulo. Por esa razón, esto es más que deseable para crear un ayudante para esto.

Encontré esta publicación que explica cómo hacerlo, pero estoy haciendo frente aquí ya que el sitio podría ser eliminado en algún momento.

Esto es para evitar que las instancias de objeto no implementen el método de instancia:: cualquier error que obtenga al intentar allowmétodos en dummyclase.

Código:

En 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

En 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

En sus especificaciones:

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
fuente