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
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).
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
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
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
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í.
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.
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.
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 do
allow(Resource).to receive(:where).with(created_from: params[:id])
.and_return(false)
end
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.
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
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.
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
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.
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.
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'.
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
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.
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.
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.
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.
Libros
<%= render "partials/books" %>Presentaciones
Recursos en internet
<%= render "partials/links" %>Screencasts
<%= render "partials/screencasts" %>Bibliotecas (documentación)
<%= render "partials/libraries" %>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)
Créditos
El documento fue iniciado por Andrea Reginato. Agradecimientos especiales a Lelylan Team. Este documento está licenciado bajo la licencia MIT.
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.