RSpec es una gran herramienta en el desarrollo guiado por comportamiento (BDD) en el proceso de escribir especificaciones legibles para humanos que dirijan y validen el desarrollo de la aplicación.

En la web hay muchos recursos que dan un panorama completo de lo qué se puede hacer con RSpec. Pero hay muy pocos recursos dedicados a cómo crear un correcto conjunto de pruebas con RSpec.

Better Specs trata de llenar este hueco recopilando muchas de las "mejores prácticas" que otros desarrolladores aprenden con años de experiencia.

Cómo describir tus métodos

Ser claro sobre qué método se está describiendo. Por ejemplo, utiliza la convención de la documentación de Ruby de . (o ::) cuando se refiere a nombres de métodos de clase y # cuando se refiere a un nombre de método de instancia.

Incorrecto

describe 'the authenticate method for User' do
describe 'if the user is an admin' do

Correcto

describe '.authenticate' do
describe '#admin?' do

Discute este punto →

Utiliza contextos

Los contextos son una gran forma de hacer que las pruebas sean claras y estén bien organizadas. A largo plazo, esta práctica mantendrá las pruebas fáciles de leer.

Incorrecto

it 'has 200 status code if logged in' do
  response.should respond_with 200
end
it 'has 401 status code if not logged in' do
  response.should respond_with 401
end

Correcto

context 'when logged in' do
  it { is_expected.to respond_with 200 }
end
context 'when logged out' do
  it { is_expected.to respond_with 401 }
end

Al describir un contexto, comienza su descripción con "cuando"(when) o "con"(with).

Discute este punto →

Mantén la descripción corta

Una descripción no debe ser mayor a 40 caracteres. Si esto pasa se deben partir usando un contexto.

Incorrecto

it 'has 422 status code if an unexpected params will be added' do

Correcto

context 'when not valid' do
  it { should respond_with 422 }
end

En el ejemplo anterior, se eliminó la descripción relacionada al código de estatus, el cual ha sido reemplazado por la expectativa. it { should respond_with 422 }. Si se ejecuta esta prueba tecleando rspec filename se obtendrá una salida legible.

Salida formateada

when not valid
  it should respond with 422

Discute este punto →

Prueba de expectativa única

El tip de 'expectativa única' es expresado más generalmente como 'cada prueba debe hacer sólo una aserción'. Esto ayuda a encontrar posibles errores, a ir directamente a la prueba fallida, y para hacer el código legible.

En expectativas (specs) unitarias aisladas, se desea que cada ejemplo especifique un (y sólo un) comportamiento. Múltiples expectativas en el mismo ejemplo son una señal de que se podrian estar especificando múltiples comportamientos.

De cualquier manera, en pruebas que no están aisladas (p.e. las que se integran con una BD, un webservice externo, o pruebas de extremo a extremo), el rendimiento se afectará gravemente por hacer la misma configuración una y otra vez, sólo por fijar una expectativa diferente en cada prueba. En este tipo de pruebas más lentas, creo que está bien especificar más de un comportamiento aislado.

Correcto (aislado)

it { should respond_with_content_type(:json) }
it { should assign_to(:resource) }

Correcto (no aislado)

it 'creates a resource' do
  response.should respond_with_content_type(:json)
  response.should assign_to(:resource)
end
Discute este punto →

Prueba todos los casos posibles

Probar es una buena práctica, pero si no se prueban los casos extremos, no será útil. Prueba casos válidos, extremos e inválidos. Por ejemplo, considera la siguiente accción.

Acción de destruir

before_filter :find_owned_resources
before_filter :find_resource

def destroy
  render 'show'
  @consumption.destroy
end

El error que normalmente veo radica en probar solamente que el recurso ha sido borrado. Pero hay al menos dos casos extremos: Cuando el recurso no se encuentra y cuando no nos pertenece. Como regla de oro, hay que pensar en todas las posibles entradas y probarlas.

Incorrecto

it 'shows the resource'

Correcto

describe '#destroy' do

  context 'when resource is found' do
    it 'responds with 200'
    it 'shows the resource'
  end

  context 'when resource is not found' do
    it 'responds with 404'
  end

  context 'when resource is not owned' do
    it 'responds with 404'
  end
end

Discute este punto →

Sintaxis Expect vs Should

En proyectos nuevos usar la sintaxis expect.

incorrecto

it 'creates a resource' do
  response.should respond_with_content_type(:json)
end

correcto

it 'creates a resource' do
  expect(response).to respond_with_content_type(:json)
end

Configura Rspec para que sólo acepte la nueva sintaxis en los proyectos nuevos, para evitar tener las 2 sintaxis por todas partes.

correcto

# spec_helper.rb
RSpec.configure do |config|
  # ...
  config.expect_with :rspec do |c|
    c.syntax = :expect
  end
end

Más información sobre la nueva sintaxis de RSpec se puede encontrar aquí y aquí.

Discute este punto →

Usa subject

Si se tienen muchas pruebas relacionadas al mismo objeto, usa subject{} para no repetir código.

Incorrecto

it { assigns('message').should match /it was born in Belville/ }

Correcto

subject { assigns('message') }
it { should match /it was born in Billville/ }

RSpec tiene la capacidad para usar un sujeto (subject) con nombre.

Correcto

subject(:hero) { Hero.first }
it "carries a sword" do
  hero.equipment.should include "sword"
end

Aprender más sobre rspec subject.

Discute este punto →

Utiliza let y let!

Cuando se tiene que asignar una variable, en lugar de usar un bloque before para crear una variable de instancia, se puede usar let. Con let la variable se carga sólo cuando es usada la primera vez en la prueba y se mantiene en caché hasta que la prueba específica termina. Una descripción muy buena y detallada de let puede ser, se puede encontrar en este enlace stackoverflow answer.

Incorrecto

describe '#type_id' do
  before { @resource = FactoryGirl.create :device }
  before { @type     = Type.find @resource.type_id }

  it 'sets the type_id field' do
    @resource.type_id.should == @type.id
  end
end

Correcto

describe '#type_id' do
  let(:resource) { FactoryGirl.create :device }
  let(:type)     { Type.find resource.type_id }

  it 'sets the type_id field' do
    resource.type_id.should == type.id
  end
end

Usa let para inicializar acciones que son "lazy loaded" para probar tus expectativas (specs).

Correcto

context 'when updates a not existing property value' do
  let(:properties) { { id: Settings.resource_id, value: 'on'} }

  def update
    resource.properties = properties
  end

  it 'raises a not found error' do
    expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
  end
end

Utiliza let! si deseas definir la variable cuando el bloque es definido. Esto puede ser útil para poblar tu base de datos para probar consultas o "scopes".

Un ejemplo de qué es realmente let.

Correcto

# this:
let(:foo) { Foo.new }

# is very nearly equivalent to this:
def foo
  @foo ||= Foo.new
end

Aprender más sobre rspec let.

Discute este punto →

Mock o no mock

Hay un debate actualmente: No (ab)usar mocks y probar comportamiento real cuando sea posible. Probar casos reales es útil cuando se actualiza el flujo de la aplicación.

Correcto

# simulate a not found resource
context "when not found" do
  before { allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false) }
  it { should respond_with 404 }
end

'Mocking' hace las especificaciones más rápidas pero son difíciles de utilizar. Pero es necesario entenderlo bien para usarlo bien. Leer más acerca de esto.

Discute este punto →

Crea sólo los datos que necesites

Si has trabajado en un proyecto de mediano tamaño (aunque también en algunos pequeños), las suites de pruebas pueden ser pesadas de ejecutarse. Para resolver este problema, es importante no cargar más datos de los necesarios. Incluso si piensas que necesitas docenas de registros, probablemente estés equivocado.

Correcto

describe "User"
  describe ".top" do
    before { FactoryGirl.create_list(:user, 3) }
    it { User.top(2).should have(2).item }
  end
end

Discute este punto →

Utiliza factories y no fixtures

Este es un tema antiguo, pero es bueno recordarlo. No uses fixtures porque son difíciles de controlar, en su lugar utiliza factories. Utilizar factories reduce la verbosidad durante la creación de nuevos datos.

Incorrecto

user = User.create(
  name: 'Genoveffa',
  surname: 'Piccolina',
  city: 'Billyville',
  birth: '17 Agoust 1982',
  active: true
)

Correcto

user = FactoryGirl.create :user

Una nota importante. Cuando hablamos de pruebas unitarias, la mejor práctica sería no utilizar ni fixtures o factories. Es mejor poner la mayoría de la lógica de negocio en bibliotecas que puedan ser probadas sin necesidad de configuraciones lentas y complejas ya sea con factories o fixtures. Leer más en este artículo

Aprender más sobre Factory Girl.

Discute este punto →

Matchers fáciles de leer

Usa matchers legibles y revisa los matchers de rspec disponibles.

Incorrecto

lambda { model.save! }.should raise_error Mongoid::Errors::DocumentNotFound

Correcto

expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound

Discute este punto →

Ejemplos compartidos

Crear pruebas es grandioso y brinda más seguridad en el día a día. Pero al final se verá código duplicado emergiendo de todas partes. Utiliza ejemplos compartidos para limpiar la suite de pruebas.

Incorrecto

describe 'GET /devices' do
  let!(:resource) { FactoryGirl.create :device, created_from: user.id }
  let(:uri) { '/devices' }

  context 'when shows all resources' do
    let!(:not_owned) { FactoryGirl.create factory }

    it 'shows all owned resources' do
      page.driver.get uri
      page.status_code.should == 200
      contains_owned_resource resource
      does_not_contain_resource not_owned
    end
  end

  describe '?start=:uri' do
    it 'shows the next page' do
      page.driver.get uri, start: resource.uri
      page.status_code.should == 200
      contains_resource resources.first
      page.should_not have_content resource.id.to_s
    end
  end
end

Correcto

describe 'GET /devices' do

  let!(:resource) { FactoryGirl.create :device, created_from: user.id }
  let(:uri)       { '/devices' }

  it_behaves_like 'a listable resource'
  it_behaves_like 'a paginable resource'
  it_behaves_like 'a searchable resource'
  it_behaves_like 'a filterable list'
end

En nuestra experiencia, los ejemplos compartidos son usados principalmente para controladores. Como los modelos son bastante diferentes entre sí, (generalmente) no comparten mucha lógica

Aprender más sobre ejemplos compartidos de rspec.

Discute este punto →

Prueba lo que ves

Prueba a profundidad tus modelos y el comportamiento de tu aplicación (integration tests). No agregues complejidad inútil probando controladores.

Cuando inicié probando mis aplicaciones, probaba controladores. Ahora ya no. Ahora, sólo creo pruebas de integración usando RSpec y Capybara. ¿Por qué? Porque tengo la certeza que se debe probar lo que se ve y porque probar controladores es un paso extra que no se necesita. La mayoría de las pruebas van dentro de los modelos y las pruebas de integración pueden agruparse fácilmente en ejemplos compartidos, formando un conjunto de pruebas limpio y legible.

Éste es un debate abierto en la comunidad Ruby y ambos puntos de vista tienen buenos argumentos para apoyar su idea. Las personas que apoyan la necesidad de probar controladores te dirán que tus pruebas de integración no cubren todos los casos de uso y que son lentas.

Ambos argumentos son incorrectos. Puedes fácilmente cubrir todos los casos de uso (¿por qué no?) y ejecutar un único archivo de especificaciones usando herramientas automatizadas como Guard. De esta forma se ejecutarán sólo las especificaciones que necesites probar rápidamente sin parar tu flujo.

Discute este punto →

No uses should (debe)

No uses la palabra should (debe) cuando describas tus pruebas. Usa la tercera persona en tiempo presente. Mejor aún, comienza a usar la nueva sintaxis con expect .

Incorrecto

it 'should not change timings' do
  consumption.occur_at.should == valid.occur_at
end

Correcto

it 'does not change timings' do
  consumption.occur_at.should == valid.occur_at
end

Ver la gema should_not para una forma de forzar esto en RSpec y la gema should_clean para una forma de limpiar ejemplos RSpec que inicien con 'should'.

Discute este punto →

Pruebas automáticas con guard

Ejecutar toda la suite de pruebas cada vez que cambia la aplicación puede ser pesado. Toma mucho tiempo y puede romper el flujo. Con Guard se puede automatizar la suite de pruebas ejecutando sólo las pruebas relacionadas con la especificación (spec) modificada, modelo, controlador o archivo sobre el que estás trabajando.

Correcto

bundle exec guard
Aquí se puede ver un ejemplo del archivo Guardfile con algunas reglas básicas de recarga.

Correcto

guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
  # run every updated spec file
  watch(%r{^spec/.+_spec\.rb$})
  # run the lib specs when a file in lib/ changes
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
  # run the model specs related to the changed model
  watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
  # run the view specs related to the changed view
  watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
  # run the integration specs related to the changed controller
  watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
  # run all integration tests when application controller change
  watch('app/controllers/application_controller.rb') { "spec/requests" }
end

Guard es una buena herramienta pero, como es normal, no siempre cubre todas las necesidades. Algunas veces el flujo de trabajo de TDD funciona mejor con un atajo de teclado que facilite la ejecución sólo de los ejemplos que se deseen y cuando se desee. Después, se puede usar una tarea de rake para ejecutar la suite de pruebas entera antes de subir el código. Aquí el atajo de teclado de vim.

Aprender más sobre guard-rspec.

Discute este punto →

Pruebas más rápidas (precarga Rails)

Cuando se ejecuta una prueba en Rails se carga la aplicación Rails completa. Esto puede tomar un tiempo y romper el flujo de desarrollo. Para resolver este problema se pueden usar soluciones como Zeus, Spin o Spork. Estas soluciones precargarán todas las bibliotecas que (usualmente) no se cambian y recargarán controladores, modelos, vistas, factories y todos los archivos que cambian más a menudo.

Aquí se puede encontrar un spec helper y una configuración Guardfile basada en Spork. Con esta configuración se recarga toda la aplicación si un archivo precargado (como inicializadores) cambia y se ejecutarán las pruebas individuales realmente rápido.

La desventaja de usar Spork es que agrega agresivamente "monkey-patches" a tu código y puedes perder varias horas tratando de entender por qué un archivo no es recargado. Si tienes algunos ejemplos de código usando Spin o alguna otra solución déjanos saberlo.

Aquí se puede encontrar un archivo de configuración Guardfile para usar con Zeus. El archivo spec_helper no requiere modificarse, sin embargo, es necesario ejectutar `zeus start` en una terminal para iniciar el servidor zeus antes de ejecutar las pruebas.

Aunque Zeus toma medidas menos agresivas que Spork, una gran desventaja son los estrictos requerimientos para usarlo; Ruby 1.9.3+ (recomendado usar la versión modificada GC de Ruby 2.0) además es requerido un sistema operativo que soporte FSEvents o inotify.

Muchos críticos están moviéndose a estas soluciones. Estas bibliotecas son un parche a un problema que es mejor resolver con un diseño mejor, e intentando cargar sólo las dependencias que necesites. Aprende más leyendo la discusión sobre el tema.

Discute este punto →

Stubbing de peticiones HTTP

Algunas veces necesitas acceder a servicios externos. En estos casos no puedes confiar en los servicios reales, pero puedes "stubearlos" con soluciones como webmock.

Correcto

context "with unauthorized access" do
  let(:uri) { 'http://api.lelylan.com/types' }
  before    { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
  it "gets a not authorized notification" do
    page.driver.get uri
    page.should have_content 'Access denied'
  end
end

Aprender más sobre webmock y VCR. Aquí una buena presentación explicando como combinarlos.

Discute este punto →

Formateador útil

Usa un formateador que brinde información útil sobre la suite de pruebas. Personalmente encuentro a fuubar muy bueno. Para hacer que funcione agrega la gema y activa fuubar como el formateador por default en tu archivo Guardfile.

Correcto

# Gemfile
group :development, :test do
  gem 'fuubar'

Correcto

# Guardfile
guard 'rspec' do
  # ...
end

Correcto

# .rspec
--drb
--format Fuubar
--color

Aprender más sobre fuubar.

Discute este punto →

Bibliotecas (documentación)

  1. Documentación de Rspec
  2. Documentación de Capybara
  3. Documentación de Factory Girl
  4. Documentación de Webmock
  5. Documentación de Timecop
  6. Shoulda Matchers
  7. Fuubar Relea

    Guía de Estilo

    Hemos buscado las mejores pautas para escribir especificaciones "agradables de leer". Un buen punto de inicio es por supuesto la suite de pruebas de Mongoid. Usa especificaciones con un estilo limpio y fácil de leer, siguiendo la mayoría de las pautas aquí descritas aquí.

    Mejorando Better Specs

    Este es un proyecto de código abierto. Si algo falta o es incorrecto sólo agrega un issue para discutir el tema. También puedes checar los siguientes issues:

    • Multilenguaje (agrega un 'issue' si deseas traducir esta guía)

    Ayúdanos

    Si has encontrado útiles estos tips y han mejorado tu trabajo, considera hacer una donación de USD $9. Cualquier donación será usada para hacer de este sitio una referencia más completa de pruebas en Ruby.