Menú principal

Shoulda matchers

05 Aug
Published by ApuX in

Tags 

Rails

Al probar modelos en Rails, solemos encontrarnos con cierto trabajo repetitivo. Para ejemplificar, supongamos un modelo que tiene dos campos: first_name y nickname. El campo first_name no debe estar vacío y el campo nickname debe tener una longitud entre 5 y 10, o bien, quedar en blanco. Nuestro modelo de Rails sería:

class Person < ActiveRecord::Base
  validates :first_name, presence: true
  validates :nickname, length: {minimum: 5, maximum: 10}, allow_blank: true
end

Las pruebas para validar la presencia del campo first_name podrían ser de la siguiente manera:

context 'validate presence of first_name' do
  it "accepts first_name with data" do
    user = Person.new first_name: 'abcd'
    user.valid?
    user.should have(:no).errors_on(:first_name)
  end
 
  it "does not accept a first_name as nil" do
    user = Person.new first_name: nil
    user.valid?
    user.should have(1).error_on(:first_name)
  end
 
  it "does not accept a blank first_name" do
    user = Person.new first_name: ''
    user.valid?
    user.should have(1).error_on(:first_name)
  end
end 

Para probar la validación de longitud del campo nickname, nuestras pruebas podrían ser como las siguientes:

context 'validate length of nickname' do
  it "accepts nickname with length 5" do
    user = Person.new nickname: 'abcde'
    user.valid?
    user.should have(:no).errors_on(:nickname)
  end
 
  it "accepts nickname with length 8" do
    user = Person.new nickname: 'abcdefgh'
    user.valid?
    user.should have(:no).errors_on(:nickname)
  end
 
  it "accepts nickname with length 10" do
    user = Person.new nickname: 'abcdefghij'
    user.valid?
    user.should have(:no).errors_on(:nickname)
  end
 
  it "accepts nickname with length 11" do
    user = Person.new nickname: 'abcdefghijk'
    user.valid?
    user.should have(1).error_on(:nickname)
  end
 
  it "accepts nickname with length 4" do
    user = Person.new nickname: 'abcd'
    user.valid?
    user.should have(1).error_on(:nickname)
  end
 
  it "accepts a nickname as nil" do
    user = Person.new nickname: nil
    user.valid?
    user.should have(:no).errors_on(:nickname)
  end
 
  it "accepts a blank nickname" do
    user = Person.new nickname: ''
    user.valid?
    user.should have(:no).errors_on(:nickname)
  end
end 

Las pruebas son más o menos completas: se prueban los casos normales y los casos límite para asegurarnos que todo funcione correctamente. Sobre este conjunto de pruebas, tal vez querramos hacer un método que genere cadenas de cierta longitud, para simplificar los ejemplos del campo nickname. Sin embargo, aun haciendo eso, en un modelo Rais común, tendremos más de un campo con el mismo comportamiento. Es decir, varios campos con validación de presencia y otros más con validación de longitud. Si sumamos todos los modelos de nuestra aplicación, nos encontraremos repitiendo el código que lo prueba modificando solamente el nombre del campo y la longitud correspondiente. Si queremos eliminar esa duplicidad, podríamos construir nuestro propio matcher, o (como haremos en esta ocasión) recurrir a una gema que ya los incluye por nosotros.

La gema es shoulda-matchers y se integra perfectamente con RSpec. En teoría, también funciona con MiniTest, pero yo no he tenido aún la oportunidad de comprobarlo, por lo que para este caso, dependemos de tener instalada la gema 'rspec-rails' para poder utilizar shoulda-matchers.

Los pasos para instalar shoulda-matchers son los de siempre: agregamos gem 'shoulda-matchers', group: :test a nuestro Gemfile y la instalamos con bundle install. Una vez instalada podemos utilizarla directamente en nuestras pruebas.

El primer ejemplo se puede refactorizar de la siguiente manera:

it { should validate_presence_of(:first_name) } 

El segundo ejemplo quedaría como sigue:

it { should ensure_length_of(:nickname).is_at_least(5).is_at_most(10) }
it { should allow_value("", nil).for(:nickname) }

Como vemos, usando shoulda-matchers podemos hacer nuestro código más descriptivo, más conciso, más fácil de leer y sin duplicación de código. Además, la sintaxis de una línea permite que nuestro código quede mucho más limpio. Si no estás familiarizado con este tipo de sintaxis, puedes revisar el post anterior

El spec completo quedaría como sigue:

describe Person do
  it { should validate_presence_of(:first_name) }
  it { should ensure_length_of(:nickname).is_at_least(5).is_at_most(10) }
  it { should allow_value("", nil).for(:nickname) }
end

En tres líneas de código cubrimos lo mismo que con los dos primeros ejemplos. Bastante bien, ¿cierto?

Además de trabajar con modelos (tanto de ActiveRecord y ActiveModel), shoulda-matchers incluye matchers para controladores. Para más información, revisa la página de github.