Inicio / Ruby / Ruby on Rails 8: Desarrollo Fullstack / Testing en Rails

Testing en Rails

Minitest vs RSpec, model/controller/integration tests, Capybara y FactoryBot.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Testing en Rails

En esta lección aprenderás a escribir tests en Rails para garantizar la calidad y estabilidad de tu aplicación. Cubriremos desde tests unitarios hasta tests de sistema.


¿Por qué testear?

Los tests son fundamentales en cualquier proyecto profesional. Permiten:

  • Detectar bugs antes de que lleguen a producción
  • Refactorizar con confianza
  • Documentar el comportamiento esperado del código
  • Facilitar la colaboración en equipo
  • Automatizar la verificación de funcionalidades

Rails viene con un framework de testing integrado desde el primer momento.


Minitest vs RSpec

Rails incluye Minitest por defecto. RSpec es una alternativa popular con una sintaxis más expresiva.

Característica Minitest RSpec
Incluido en Rails ✅ Sí ❌ Gema externa
Sintaxis Clásica (métodos) DSL (describe/it)
Velocidad Más rápido Ligeramente más lento
Comunidad Oficial Rails Muy popular
Curva de aprendizaje Menor Mayor al inicio

Ejemplo Minitest:

class UserTest < ActiveSupport::TestCase
  test "debe tener un nombre válido" do
    user = User.new(name: "", email: "test@test.com")
    assert_not user.valid?
    assert_includes user.errors[:name], "can't be blank"
  end
end

Ejemplo RSpec:

RSpec.describe User, type: :model do
  it "debe tener un nombre válido" do
    user = User.new(name: "", email: "test@test.com")
    expect(user).not_to be_valid
    expect(user.errors[:name]).to include("can't be blank")
  end
end

Nota: En este curso usaremos Minitest por ser el estándar de Rails, pero los conceptos aplican a ambos frameworks.


Estructura del directorio de tests

test/
├── controllers/          # Tests de controladores
│   └── articles_controller_test.rb
├── fixtures/             # Datos de prueba en YAML
│   ├── articles.yml
│   └── users.yml
├── helpers/              # Tests de helpers
├── integration/          # Tests de integración
│   └── user_flows_test.rb
├── mailers/              # Tests de mailers
│   └── user_mailer_test.rb
├── models/               # Tests de modelos
│   └── user_test.rb
├── system/               # Tests de sistema (navegador)
│   └── articles_test.rb
├── test_helper.rb        # Configuración global de tests
└── application_system_test_case.rb

Tests de modelos

Los tests de modelos verifican validaciones, asociaciones, métodos y scopes:

# test/models/user_test.rb
require "test_helper"

class UserTest < ActiveSupport::TestCase
  test "usuario válido con todos los campos" do
    user = User.new(
      name: "María López",
      email: "maria@example.com",
      password: "password123"
    )
    assert user.valid?
  end

  test "no es válido sin email" do
    user = User.new(name: "María", password: "password123")
    assert_not user.valid?
    assert_includes user.errors[:email], "can't be blank"
  end

  test "email debe ser único" do
    User.create!(name: "Ana", email: "ana@test.com", password: "pass123")
    duplicate = User.new(name: "Otra Ana", email: "ana@test.com", password: "pass456")
    assert_not duplicate.valid?
  end

  test "nombre completo combina nombre y apellido" do
    user = User.new(first_name: "Carlos", last_name: "García")
    assert_equal "Carlos García", user.full_name
  end

  test "scope activos retorna solo usuarios activos" do
    active_count = User.active.count
    assert active_count >= 0
  end

  test "puede tener muchos artículos" do
    user = users(:maria)
    assert_respond_to user, :articles
  end
end

Ejecutar tests de modelos:

bin/rails test test/models/

# Un archivo específico
bin/rails test test/models/user_test.rb

# Un test específico por línea
bin/rails test test/models/user_test.rb:10

Fixtures

Las fixtures son datos de prueba definidos en archivos YAML:

# test/fixtures/users.yml
maria:
  name: María López
  email: maria@example.com
  password_digest: <%= BCrypt::Password.create("password123") %>
  role: admin

carlos:
  name: Carlos García
  email: carlos@example.com
  password_digest: <%= BCrypt::Password.create("password123") %>
  role: user
# test/fixtures/articles.yml
primer_articulo:
  title: Introducción a Rails
  body: Rails es un framework web para Ruby...
  published: true
  user: maria

borrador:
  title: Artículo en borrador
  body: Este artículo aún no está publicado...
  published: false
  user: carlos

Accede a las fixtures en los tests:

test "fixture maria existe y es admin" do
  user = users(:maria)
  assert_equal "María López", user.name
  assert_equal "admin", user.role
end

Fixtures vs Factories (FactoryBot)

FactoryBot es una alternativa a las fixtures que permite crear objetos de prueba de forma más flexible:

# Gemfile (grupo test)
gem "factory_bot_rails"
# test/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Usuario de prueba" }
    sequence(:email) { |n| "user#{n}@test.com" }
    password { "password123" }

    trait :admin do
      role { "admin" }
    end

    trait :with_articles do
      after(:create) do |user|
        create_list(:article, 3, user: user)
      end
    end
  end
end
# En los tests
test "crear usuario con factory" do
  user = create(:user)
  assert user.persisted?

  admin = create(:user, :admin)
  assert_equal "admin", admin.role

  user_con_articulos = create(:user, :with_articles)
  assert_equal 3, user_con_articulos.articles.count
end

Tip: Las fixtures son más rápidas (se cargan una vez en transacción), pero las factories son más flexibles. Usa fixtures para datos base y factories para escenarios específicos.


Tests de controladores

# test/controllers/articles_controller_test.rb
require "test_helper"

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:maria)
    @article = articles(:primer_articulo)
  end

  test "debe obtener index" do
    get articles_url
    assert_response :success
  end

  test "debe mostrar artículo" do
    get article_url(@article)
    assert_response :success
    assert_select "h1", @article.title
  end

  test "debe crear artículo cuando está autenticado" do
    sign_in @user

    assert_difference("Article.count", 1) do
      post articles_url, params: {
        article: { title: "Nuevo artículo", body: "Contenido...", published: true }
      }
    end

    assert_redirected_to article_url(Article.last)
  end

  test "no debe crear artículo sin autenticación" do
    post articles_url, params: {
      article: { title: "Nuevo", body: "Contenido" }
    }
    assert_redirected_to login_url
  end

  test "debe actualizar artículo" do
    sign_in @user
    patch article_url(@article), params: {
      article: { title: "Título actualizado" }
    }
    assert_redirected_to article_url(@article)
    @article.reload
    assert_equal "Título actualizado", @article.title
  end

  test "debe eliminar artículo" do
    sign_in @user
    assert_difference("Article.count", -1) do
      delete article_url(@article)
    end
    assert_redirected_to articles_url
  end
end

Tests de integración

Los tests de integración verifican flujos completos de usuario:

# test/integration/user_registration_flow_test.rb
require "test_helper"

class UserRegistrationFlowTest < ActionDispatch::IntegrationTest
  test "flujo completo de registro y login" do
    # Visitar página de registro
    get signup_url
    assert_response :success

    # Registrarse
    assert_difference("User.count", 1) do
      post users_url, params: {
        user: {
          name: "Nuevo Usuario",
          email: "nuevo@test.com",
          password: "password123",
          password_confirmation: "password123"
        }
      }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_select ".flash-notice", "Cuenta creada exitosamente"

    # Cerrar sesión
    delete logout_url
    assert_redirected_to root_url

    # Iniciar sesión
    post login_url, params: { email: "nuevo@test.com", password: "password123" }
    assert_redirected_to dashboard_url
    follow_redirect!
    assert_select "h1", /Dashboard/
  end
end

Tests de sistema (Capybara)

Los tests de sistema simulan interacciones reales del usuario en el navegador:

# test/application_system_test_case.rb
require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome, screen_size: [1400, 900]
end
# test/system/articles_test.rb
require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  setup do
    @user = users(:maria)
  end

  test "crear un artículo nuevo" do
    visit login_url
    fill_in "Email", with: @user.email
    fill_in "Contraseña", with: "password123"
    click_on "Iniciar sesión"

    visit new_article_url
    fill_in "Título", with: "Mi artículo de prueba"
    fill_in "Contenido", with: "Este es el contenido del artículo."
    check "Publicado"
    click_on "Crear artículo"

    assert_text "Artículo creado exitosamente"
    assert_text "Mi artículo de prueba"
  end

  test "buscar artículos" do
    visit articles_url
    fill_in "Buscar", with: "Rails"
    click_on "Buscar"

    assert_selector ".article-card", minimum: 1
  end
end

Ejecutar tests de sistema:

bin/rails test:system

Assertions comunes

# Igualdad
assert_equal expected, actual
assert_not_equal unexpected, actual

# Verdadero / Falso
assert condition
assert_not condition

# Nil
assert_nil object
assert_not_nil object

# Inclusión
assert_includes collection, item

# Diferencia en base de datos
assert_difference("Model.count", 1) { create_action }
assert_no_difference("Model.count") { failed_action }

# Respuesta HTTP
assert_response :success      # 200
assert_response :redirect     # 3xx
assert_response :not_found    # 404

# Redirecciones
assert_redirected_to path

# Selectores HTML
assert_select "h1", "Texto esperado"
assert_select ".clase", count: 3

# Excepciones
assert_raises(ActiveRecord::RecordNotFound) { action }

# Emails
assert_emails 1 do
  UserMailer.welcome(user).deliver_now
end

Test Helpers personalizados

# test/test_helper.rb
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"

module ActiveSupport
  class TestCase
    parallelize(workers: :number_of_processors)
    fixtures :all

    # Helper para autenticación en tests
    def sign_in(user)
      post login_url, params: { email: user.email, password: "password123" }
    end

    # Helper para crear datos de prueba
    def create_published_article(user:, title: "Test Article")
      Article.create!(title: title, body: "Contenido", published: true, user: user)
    end
  end
end

Configurar CI (Integración Continua)

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Configurar base de datos
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost/test
          RAILS_ENV: test
        run: |
          bin/rails db:create
          bin/rails db:schema:load

      - name: Ejecutar tests
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost/test
          RAILS_ENV: test
        run: bin/rails test

      - name: Ejecutar tests de sistema
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost/test
          RAILS_ENV: test
        run: bin/rails test:system

Resumen

  • Rails incluye Minitest por defecto; RSpec es una alternativa popular.
  • Organiza tus tests en carpetas: models/, controllers/, integration/, system/.
  • Usa fixtures para datos base y FactoryBot para escenarios específicos.
  • Los tests de modelo verifican validaciones, asociaciones y lógica de negocio.
  • Los tests de controlador verifican respuestas HTTP y flujos de datos.
  • Los tests de sistema con Capybara simulan interacciones del usuario en el navegador.
  • Configura CI con GitHub Actions para ejecutar tests automáticamente en cada push.
  • Ejecuta tests con bin/rails test y tests de sistema con bin/rails test:system.
🔒

Ejercicio práctico disponible

Mini framework de testing al estilo RSpec

Desbloquear ejercicios
// Mini framework de testing al estilo RSpec
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes