Testing en Ruby: RSpec, FactoryBot y la cultura BDD del ecosistema

La comunidad Ruby tiene una relación con el testing que no se ve igual en otros lenguajes. Rails viene con una suite de tests desde el primer día, las gemas publican sus tests como parte del código, y hay una cultura implícita de que el código sin tests está incompleto. RSpec y FactoryBot son las dos herramientas más usadas para construir esa cobertura en aplicaciones Rails.

RSpec: testing con estilo BDD

RSpec es un framework de testing que organiza los tests como especificaciones del comportamiento esperado. La diferencia con Minitest (el framework incluido en Ruby) no es solo sintáctica: RSpec te hace pensar en el «qué hace» antes que en el «cómo lo hace».

# Instalar
# Gemfile
group :development, :test do
  gem 'rspec-rails'
end

# rails generate rspec:install

# spec/models/usuario_spec.rb
RSpec.describe Usuario, type: :model do
  describe '#nombre_completo' do
    context 'cuando tiene nombre y apellido' do
      it 'devuelve nombre y apellido unidos' do
        usuario = Usuario.new(nombre: 'Ana', apellido: 'García')
        expect(usuario.nombre_completo).to eq('Ana García')
      end
    end

    context 'cuando solo tiene nombre' do
      it 'devuelve solo el nombre' do
        usuario = Usuario.new(nombre: 'Ana', apellido: nil)
        expect(usuario.nombre_completo).to eq('Ana')
      end
    end
  end

  describe '#mayor_de_edad?' do
    it 'devuelve true para usuarios de 18 o más años' do
      usuario = Usuario.new(fecha_nacimiento: 20.years.ago)
      expect(usuario.mayor_de_edad?).to be true
    end

    it 'devuelve false para usuarios menores de 18' do
      usuario = Usuario.new(fecha_nacimiento: 16.years.ago)
      expect(usuario.mayor_de_edad?).to be false
    end
  end
end

La estructura describe / context / it organiza los tests en una jerarquía legible. describe agrupa por método o funcionalidad, context describe las condiciones previas, it especifica el comportamiento esperado.

Matchers: la expresividad de RSpec

Los matchers de RSpec hacen que las aserciones sean más legibles que los assert_equal de Minitest:

# Igualdad
expect(resultado).to eq(42)
expect(lista).to include('elemento')
expect(texto).to start_with('Hola')
expect(texto).to end_with('mundo')

# Predicados
expect(usuario).to be_activo    # Llama a usuario.activo?
expect(coleccion).to be_empty
expect(coleccion).to be_present

# Errores
expect { metodo_que_falla }.to raise_error(ArgumentError)
expect { metodo_que_falla }.to raise_error(ArgumentError, 'mensaje exacto')

# Cambios en la base de datos
expect {
  post :create, params: { usuario: atributos_validos }
}.to change(Usuario, :count).by(1)

# Mocks y stubs
allow(servicio).to receive(:llamada_externa).and_return('respuesta')
expect(mailer).to receive(:enviar).once

FactoryBot: datos de prueba sin dolores de cabeza

Los tests necesitan datos. La solución básica es crear objetos directamente en cada test, pero cuando los modelos tienen muchas validaciones y relaciones, eso se vuelve tedioso. FactoryBot resuelve esto definiendo plantillas (factories) para crear objetos de prueba con valores por defecto razonables.

# spec/factories/usuarios.rb
FactoryBot.define do
  factory :usuario do
    nombre { 'Ana' }
    apellido { 'García' }
    sequence(:email) { |n| "ana#{n}@example.com" }
    fecha_nacimiento { 25.years.ago }
    activo { true }

    # Trait: variación del factory base
    trait :inactivo do
      activo { false }
    end

    trait :menor do
      fecha_nacimiento { 15.years.ago }
    end

    # Factory derivado
    factory :admin do
      rol { 'admin' }
    end
  end
end
# Uso en los tests
# Crear objeto con valores por defecto
usuario = create(:usuario)

# Sobreescribir atributos
usuario = create(:usuario, nombre: 'Carlos', email: '[email protected]')

# Usar traits
usuario_inactivo = create(:usuario, :inactivo)
menor = create(:usuario, :menor)

# Construir sin guardar en BBDD (más rápido)
usuario = build(:usuario)

# Atributos solo (sin objeto)
atributos = attributes_for(:usuario)

# Crear varios
usuarios = create_list(:usuario, 5)

La secuencia sequence(:email) genera emails únicos automáticamente, lo que evita violaciones de constraints de unicidad al crear múltiples usuarios en un mismo test.

Organización de la suite de tests

En una aplicación Rails con RSpec, los tests se organizan por tipo:

  • spec/models/: tests unitarios de modelos, validaciones y métodos.
  • spec/requests/: tests de integración de los endpoints HTTP (sustituto moderno de controller specs).
  • spec/system/: tests end-to-end con Capybara y un navegador headless (Selenium, Cuprite).
  • spec/services/: tests de clases de servicio.
  • spec/jobs/: tests de jobs de Active Job.
# spec/requests/usuarios_spec.rb
RSpec.describe 'Usuarios', type: :request do
  describe 'GET /usuarios/:id' do
    let(:usuario) { create(:usuario) }

    it 'devuelve el usuario en JSON' do
      get usuario_path(usuario), headers: { 'Accept' => 'application/json' }

      expect(response).to have_http_status(:ok)
      expect(JSON.parse(response.body)['nombre']).to eq(usuario.nombre)
    end

    it 'devuelve 404 si el usuario no existe' do
      get usuario_path(id: 99999), headers: { 'Accept' => 'application/json' }
      expect(response).to have_http_status(:not_found)
    end
  end
end

Shared examples y helpers

RSpec tiene mecanismos para reutilizar especificaciones entre tests:

# spec/support/shared_examples/recurso_autenticado.rb
RSpec.shared_examples 'recurso que requiere autenticación' do
  context 'sin sesión' do
    it 'redirige al login' do
      subject
      expect(response).to redirect_to(login_path)
    end
  end
end

# Uso en un spec
RSpec.describe 'Dashboard', type: :request do
  describe 'GET /dashboard' do
    subject { get dashboard_path }
    it_behaves_like 'recurso que requiere autenticación'
  end
end

Para ver cómo se aplica BDD en otro stack, el artículo sobre TDD con PHP y Laravel muestra un enfoque similar con PHPUnit y las diferencias culturales entre los dos ecosistemas.

Velocidad: el talón de Aquiles

Las suites de RSpec en aplicaciones Rails grandes pueden ser lentas. Algunas estrategias habituales para mantenerlas manejables:

  • Usar build en lugar de create cuando no necesitas persistir en BBDD.
  • Ejecutar en paralelo con parallel_tests.
  • Evitar DatabaseCleaner con la estrategia :truncation; usar transacciones (:transaction) siempre que sea posible.
  • Limitar los tests de sistema a los flujos más críticos y usar request specs para el resto.

Una suite bien organizada con RSpec y FactoryBot puede cubrir el 90% de los casos sin ejecutar un navegador, lo que marca la diferencia entre tests que se ejecutan en 30 segundos y tests que tardan 10 minutos.

Imagen: Pexels / Jakub Zerdzicki

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP