DevTools

Cheatsheet Ruby on Rails

Framework web Ruby full-stack com convenção sobre configuração

Volver a los lenguajes
Ruby on Rails
75 tarjetas encontradas
Categorías:
Versiones:

Setup e CLI


9 cards
Instalar Rails
gem install rails
rails new mi_app
cd mi_app
rails server

gem install rails instala el framework globalmente. rails new genera la estructura completa del proyecto. rails server inicia Puma en localhost:3000.

Comandos CLI esenciales
rails console        # IRB con la app cargada
rails db:migrate     # aplicar migrations
rails db:seed        # poblar BD
rails routes         # listar todas las rutas
rails dbconsole      # acceder al SQL directo

La rails console carga todo el entorno (models, configs). rails routes muestra verbos, paths y controllers mapeados. dbconsole abre el cliente SQL nativo.

Configuraciones
# config/application.rb
config.time_zone = "Lisbon"
config.i18n.default_locale = :es

# config/credentials.yml.enc
rails credentials:edit
Rails.application.credentials.secret_api_key

config/application.rb define settings globales. credentials guarda secretos cifrados (API keys, passwords) — más seguro que variables de entorno en archivo.

Generar model
rails g model Post título:string texto:text
rails g model User nombre:string email:string

# Genera:
#   app/models/post.rb
#   db/migrate/xxxx_create_posts.rb

rails g model crea el archivo del model y la migration automáticamente. Los tipos (string, text, integer) definen las columnas de la tabla.

Estructura del proyecto
app/
  models/          # ActiveRecord
  controllers/     # lógica HTTP
  views/           # templates ERB
  helpers/         # view helpers
config/routes.rb   # rutas
db/migrate/        # migrations
Gemfile            # dependencias

Rails sigue el patrón MVC: models/ (datos), controllers/ (lógica), views/ (presentación). config/routes.rb mapea URLs a controllers.

Generar controller
rails g controller Posts index show new

# Genera:
#   app/controllers/posts_controller.rb
#   app/views/posts/ (index, show, new)
#   rutas en config/routes.rb

rails g controller crea el controller, las views y añade las rutas. Las actions listadas generan métodos vacíos y templates correspondientes.

Gemfile
# Gemfile
gem "rails", "~> 7.1"
gem "sqlite3"
gem "puma"

group :development do
  gem "debug"
end

bundle install
bundle add devise

El Gemfile lista las dependencias. bundle install instala todo. group :development limita gems a entornos específicos. bundle add añade e instala en un solo paso.

Scaffold
rails g scaffold Post título:string texto:text

# Genera todo: model, controller,
# views, migration, rutas, tests
rails db:migrate

scaffold genera el CRUD completo de una vez: model, controller con 7 actions, views, migration y rutas. Ideal para prototipado rápido.

Entornos
# config/environments/
#   development.rb
#   test.rb
#   production.rb

RAILS_ENV=production rails server
Rails.env                # => "development"
Rails.env.production?    # => false

Rails tiene 3 entornos: development, test y production. Cada uno tiene configs propias en config/environments/. Rails.env devuelve el entorno activo.

Models e ActiveRecord


9 cards
Definir model
class Post < ApplicationRecord
  validates :título, presence: true
  belongs_to :autor
  has_many :comentarios
end

Todo model hereda de ApplicationRecord. El nombre de la clase es singular (Post) y la tabla es plural (posts). validates y asociaciones se declaran en el cuerpo de la clase.

Asociaciones
has_many :comentarios
belongs_to :post
has_one :perfil
has_many :tags, through: :taggings
has_and_belongs_to_many :categorias

has_many / belongs_to son las más comunes. through crea asociación vía tabla intermedia. has_and_belongs_to_many usa join table sin model propio.

Eager loading
# Problema N+1:
Post.all.each { |p| p.autor.nombre }

# Solución:
Post.includes(:autor).each { |p|
  p.autor.nombre }

Post.joins(:autor).where(
  autors: { activo: true })

includes carga asociaciones en 2 queries (evita N+1). joins hace INNER JOIN — necesario para filtrar por campos de la asociación. preload fuerza 2 queries separadas.

Validaciones
validates :email, presence: true,
  uniqueness: true,
  format: { with: URI::MailTo::EMAIL_REGEXP }

validates :edad, numericality: {
  greater_than: 0, only_integer: true }

validates :título, length: { maximum: 200 }

validates acepta múltiples reglas: presence, uniqueness, format, numericality, length. Si falla, save devuelve false y errors se llena.

Scopes
scope :activos, -> { where(activo: true) }
scope :recientes, -> {
  order(creado_en: :desc).limit(5) }

Post.activos.recientes

scope define queries con nombre reutilizables con lambda. Pueden encadenarse como métodos normales. Equivalentes a métodos de clase que devuelven ActiveRecord::Relation.

CRUD básico
Post.create(título: "Hola", texto: "Mundo")
Post.find(1)
Post.find_by(título: "Hola")
post.update(título: "Nuevo título")
post.destroy

create instancia y guarda. find lanza RecordNotFound si no existe; find_by devuelve nil. update valida y guarda. destroy elimina el registro.

Callbacks
class Post < ApplicationRecord
  before_save :normalizar_título
  after_create :notificar

  private

  def normalizar_título
    self.título = título.strip.downcase
  end
end

Callbacks ejecutan código en puntos del ciclo de vida: before_save, after_create, before_destroy. Útiles para normalización, logs y notificaciones automáticas.

Queries y condiciones
Post.where(activo: true)
Post.where("views > ?", 100)
Post.order(creado_en: :desc)
Post.limit(10).offset(20)
Post.count
Post.pluck(:título)

where acepta hash o SQL con placeholders ?. Las queries son encadenables (lazy). pluck devuelve solo los valores de una columna. count hace SELECT COUNT(*).

Enum
class Post < ApplicationRecord
  enum :status, { borrador: 0,
    publicado: 1, archivado: 2 }
end

post.publicado!
post.status        # => "publicado"
Post.publicado     # => todos los publicados

enum mapea enteros a nombres legibles. Genera métodos de query (Post.publicado) y de transición (post.publicado!). La columna debe ser integer en la BD.

Controllers


9 cards
Actions RESTful
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id])
  end
end

Las 7 actions RESTful: index, show, new, create, edit, update, destroy. Las variables con @ quedan disponibles en la view.

Flash y redirect
flash[:notice] = "¡Post creado!"
flash[:alert] = "Error al guardar."
redirect_to posts_path

redirect_to @post, notice: "OK"
redirect_to root_path, alert: "Denegado"

flash guarda mensajes para el siguiente request. notice (éxito) y alert (error) son atajos inline en redirect_to. El mensaje desaparece tras mostrarse.

Skip y herencia
class ApplicationController < ActionController::Base
  before_action :autenticar!
end

class PublicController < ApplicationController
  skip_before_action :autenticar!
end

ApplicationController es el padre de todos — los filtros definidos aquí se aplican globalmente. skip_before_action quita un filtro heredado en controllers específicos.

Strong parameters
def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to @post, notice: "¡Creado!"
  else
    render :new, status: :unprocessable_entity
  end
end

private

def post_params
  params.require(:post).permit(:título, :texto)
end

params.require(:post) exige la clave. permit lista los campos aceptados — protege contra mass assignment. Sin esto, se lanza ActiveModel::ForbiddenAttributesError.

Render vs Redirect
# Render: misma request, sin redirect
render :new
render partial: "form"
render plain: "OK"

# Redirect: nueva request HTTP
redirect_to posts_path

render muestra una view sin cambiar la URL (misma request). redirect_to envía HTTP 302 — el navegador hace un nuevo GET. Usa render en errores de validación para mantener los datos del form.

Before actions
before_action :set_post, only: [:show,
  :edit, :update, :destroy]
before_action :autenticar!

private

def set_post
  @post = Post.find(params[:id])
end

before_action se ejecuta antes de las actions. only / except limitan dónde corre. Ideal para cargar recursos y verificar autenticación sin repetir código.

Rescue y errores
rescue_from ActiveRecord::RecordNotFound,
  with: :no_encontrado

private

def no_encontrado
  render file: "public/404.html",
    status: :not_found
end

rescue_from captura excepciones globalmente en el controller. Evita repetir begin/rescue. Puede renderizar páginas de error o redirigir con mensajes.

Respond_to
def show
  @post = Post.find(params[:id])
  respond_to do |format|
    format.html
    format.json { render json: @post }
    format.xml  { render xml: @post }
  end
end

respond_to permite responder en múltiples formatos según el header Accept. El bloque define la respuesta para cada formato. La extensión de la URL (.json) también lo activa.

Cookies y session
session[:user_id] = user.id
session[:user_id]      # => 42
session.delete(:user_id)

cookies[:tema] = "oscuro"
cookies.permanent[:recordar] = token

session guarda datos temporales (se limpia al cerrar el navegador). cookies persiste en el cliente. cookies.permanent expira en 20 años. Ambos accesibles vía hash.

Routes


8 cards
Resources
resources :posts
# Genera 7 rutas: index, show, new,
# create, edit, update, destroy

resources :posts, only: [:index, :show]
resources :posts, except: [:destroy]

resources genera las 7 rutas RESTful automáticamente. only / except limitan cuáles se crean. Cada ruta tiene un helper con nombre (posts_path, post_path).

Namespace
namespace :admin do
  resources :users
  resources :posts
end

# /admin/users
# Admin::UsersController

namespace agrupa rutas bajo un prefijo y módulo. Crea controllers en app/controllers/admin/. Los helpers ganan prefijo: admin_users_path.

Rutas personalizadas
get "acerca", to: "pages#acerca"
post "login", to: "sessions#create"
root "posts#index"

get "posts/:id/preview",
  to: "posts#preview"

get, post, put, delete definen rutas manuales. root define la página inicial. :id es un parámetro dinámico accesible vía params[:id].

Scope y shallow
scope "/api", module: "api" do
  resources :posts
end

resources :posts, shallow: true do
  resources :comentarios
end
# /comentarios/5 (sin /posts/1)

scope personaliza prefijo/módulo sin crear un namespace completo. shallow: true anida solo index/new/create — las restantes quedan en el nivel raíz.

Nested resources
resources :posts do
  resources :comentarios
end

# /posts/1/comentarios
# /posts/1/comentarios/5

Los recursos anidados generan URLs jerárquicas. El controller recibe params[:post_id]. Limita el nesting a 1 nivel para mantener URLs legibles.

Member y collection
resources :posts do
  member do
    get :preview
  end
  collection do
    get :archivados
  end
end
# /posts/1/preview
# /posts/archivados

member añade rutas para un recurso específico (con :id). collection añade rutas para la colección (sin :id). Generan helpers automáticos.

URL helpers
posts_path          # /posts
post_path(@post)    # /posts/1
new_post_path       # /posts/new
edit_post_path(@post) # /posts/1/edit

posts_url           # http://host/posts

Cada ruta genera helpers: _path (relativo) y _url (absoluto). Usa _path en views y _url en emails y redirects externos.

Constraints
constraints subdomain: "api" do
  resources :posts
end

get "posts/:id", to: "posts#show",
  constraints: { id: /\d+/ }

constraints restringe rutas por subdominio, formato o regex. Útil para APIs en subdominios o para validar parámetros directamente en la ruta.

Views e ERB


9 cards
ERB básico
<h1><%= @post.título %></h1>
<p><%= @post.texto %></p>

<%# comentario (no renderiza) %>
<% if @post.activo? %>
  <span>Publicado</span>
<% end %>

<%= %> imprime el resultado (con escape HTML). <% %> ejecuta sin imprimir. <%# %> es comentario. Las variables @ del controller quedan disponibles.

Layouts y yield
<!-- layouts/application.html.erb -->
<body>
  <%= yield %>
  <%= yield :sidebar %>
</body>

<!-- En la view: -->
<% content_for :sidebar do %>
  <nav>Menú lateral</nav>
<% end %>

yield inserta el contenido de la view en el layout. yield :sidebar inserta contenido con nombre. content_for define bloques para slots específicos del layout.

Turbo y Stimulus
<%= turbo_frame_tag "posts" do %>
  <%= render @posts %>
<% end %>

<%= link_to "Siguiente", posts_path,
  data: { turbo_frame: "posts" } %>

Turbo Frames actualizan partes de la página sin reload completo. turbo_frame_tag define una región actualizable. Los enlaces con data-turbo-frame cargan dentro del frame.

Iteración
<% @posts.each do |post| %>
  <li><%= link_to post.título, post %></li>
<% end %>

<%= render @posts %>

each itera sobre colecciones. render @posts renderiza automáticamente el partial _post.html.erb para cada ítem — más limpio que un loop manual.

Asset helpers
<%= image_tag "logo.png", alt: "Logo" %>
<%= stylesheet_link_tag "app" %>
<%= javascript_include_tag "app" %>
<%= link_to "Ver post", post_path(@post) %>

image_tag, stylesheet_link_tag y javascript_include_tag generan tags con paths del asset pipeline. link_to crea enlaces con helpers de ruta.

Partials
<%= render "form" %>
<%= render "posts/card", post: @post %>

<!-- app/views/posts/_card.html.erb -->
<div class="card">
  <%= post.título %>
</div>

Los partials son archivos con prefijo _. render "form" búsqueda _form.html.erb en la misma carpeta. Pasa variables locales como segundo argumento (hash).

Number y date helpers
<%= number_to_currency(19.99) %>
<%= number_with_delimiter(1000000) %>
<%= time_ago_in_words(@post.created_at) %>
<%= l(@post.created_at, format: :long) %>

number_to_currency formatea valores monetarios. time_ago_in_words devuelve "hace 3 días". l() formatea fechas según el locale activo.

Form helpers
<%= form_with model: @post do |f| %>
  <%= f.label :título %>
  <%= f.text_field :título %>
  <%= f.text_area :texto, rows: 5 %>
  <%= f.select :status, ["borrador", "publicado"] %>
  <%= f.submit "Guardar" %>
<% end %>

form_with model: genera un form para create o update automáticamente. f.text_field, f.text_area, f.select crean inputs con los nombres correctos. El CSRF token se incluye automáticamente.

Content tags y safe
<%= content_tag :div, "Hola", class: "msg" %>
<%= tag.br %>
<%= tag.input type: "text", name: "q" %>

<%= raw @html %>
<%= sanitize @html, tags: %w[b i em] %>

content_tag genera elementos HTML dinámicamente. tag.br crea tags self-closing. raw imprime sin escape (¡cuidado!). sanitize elimina tags peligrosas manteniendo las seguras.

Migrations


8 cards
Crear migration
rails g migration CreatePosts \
  título:string texto:text activo:boolean

rails g migration AddViewsToPosts \
  views:integer

rails g migration genera un archivo con timestamp. Los nombres CreateX generan create_table. Los nombres AddXToY generan add_column. El timestamp define el orden de ejecución.

Change reversible
def change
  create_table :posts do |t|
    t.string :título, null: false
    t.text :texto
    t.timestamps
  end
end

def change es auto-reversible — Rails sabe invertir create_table, add_column, etc. Usa def up / def down solo para operaciones irreversibles.

Tipos de columna
t.string   :título
t.text     :cuerpo
t.integer  :views
t.float    :rating
t.decimal  :precio, precision: 8, scale: 2
t.boolean  :activo, default: false
t.date     :publicado_en
t.datetime :created_at
t.references :autor, foreign_key: true

Los tipos mapean a SQL: string (varchar), text (sin límite), decimal (precisión exacta). references crea la columna autor_id con foreign key.

Timestamps y defaults
t.timestamps
# Crea: created_at + updated_at

t.boolean :activo, default: true
t.string :status, default: "borrador"
t.integer :views, default: 0

t.timestamps crea created_at y updated_at (actualizado automáticamente). default define el valor inicial de la columna. Rails gestiona los timestamps sin código extra.

Alterar esquema
add_column :posts, :views, :integer
remove_column :posts, :views
rename_column :posts, :old, :new
add_index :posts, :título
change_column_null :posts, :título, false

add_column / remove_column añaden/eliminan columnas. add_index mejora el rendimiento de las queries. change_column_null hace la columna obligatoria (NOT NULL).

Índices y constraints
add_index :posts, :título, unique: true
add_index :posts, [:autor_id, :created_at]

add_foreign_key :posts, :autors
add_check_constraint :posts,
  "views >= 0", name: "views_positivas"

add_index con unique: true impide duplicados. Los índices compuestos (array) optimizan queries con múltiples columnas. add_check_constraint valida a nivel de BD.

Comandos de gestión
rails db:migrate
rails db:rollback STEP=1
rails db:migrate:status
rails db:reset
rails db:schema:load

db:migrate aplica las pendientes. db:rollback revierte la última (o N con STEP). migrate:status muestra cuáles se aplicaron. db:reset hace drop + create + migrate.

Seed de datos
# db/seeds.rb
Post.create!(título: "Primero",
  texto: "Contenido inicial")

5.times do |i|
  Post.create!(título: "Post #{i}")
end

# Ejecutar:
rails db:seed

db/seeds.rb puebla la BD con datos iniciales. create! (con bang) lanza excepción si la validación falla. Ejecuta con rails db:seed — seguro para idempotencia.

Auth e Segurança


8 cards
has_secure_password
gem "bcrypt"

class User < ApplicationRecord
  has_secure_password
end

user.authenticate("clave123")

has_secure_password añade hash bcrypt y el método authenticate. Requiere columna password_digest. Devuelve el user si la contraseña coincide, false en caso contrario.

Pundit (autorización)
gem "pundit"

class PostPolicy < ApplicationPolicy
  def update?
    user.admin? || record.autor == user
  end
end

authorize @post

Pundit hace autorización con policies. Cada model tiene una clase Policy con métodos booleanos. authorize en el controller verifica el permiso — lanza NotAuthorizedError si se deniega.

Devise
gem "devise"
rails g devise:install
rails g devise User
rails db:migrate

# Helpers disponibles:
current_user
user_signed_in?
authenticate_user!

Devise es la gem de auth más popular. Genera registro, login, recuperación de contraseña y confirmación de email. authenticate_user! como before_action protege rutas.

SQL injection
# SEGURO (placeholder):
Post.where("título LIKE ?", "%#{q}%")

# PELIGROSO (nunca hacer):
Post.where("título LIKE '%#{q}%'")

# SEGURO (hash):
Post.where(título: params[:título])

Usa siempre placeholders ? o hash en where. La interpolación directa permite SQL injection. ActiveRecord hace escape automático con placeholders.

CSRF protection
# ApplicationController
protect_from_forgery with: :exception

# En el layout (automático):
<%= csrf_meta_tags %>

# En formularios: token incluido

Rails protege contra CSRF por defecto. csrf_meta_tags inserta el token en el HTML. Los forms generados con form_with incluyen el token automáticamente. Las APIs usan skip_forgery_protection.

Headers de seguridad
# config/application.rb
config.action_dispatch.default_headers = {
  "X-Frame-Options" => "DENY",
  "X-Content-Type-Options" => "nosniff",
  "X-XSS-Protection" => "1; mode=block"
}

Rails ya incluye headers seguros por defecto. X-Frame-Options previene clickjacking. X-Content-Type-Options evita MIME sniffing. Personaliza en default_headers.

XSS y sanitize
<%= @texto %>          # escapa HTML
<%= raw @html %>       # NO escapa
<%= sanitize @html,
  tags: %w[b i em p] %>

ERB escapa HTML automáticamente con <%= %>. raw desactiva el escape (peligroso). sanitize elimina tags/scripts maliciosos manteniendo una whitelist segura.

Rate limiting
# config/initializers/rate_limit.rb
Rails.application.config.action_dispatch
  .rate_limit = {
    limit: 100,
    period: 1.minute
  }

# O manual con Rack::Attack

Rails 7.1+ tiene rate limiting nativo. Limita requests por IP/período. Para control fino, usa Rack::Attack (gem) con reglas por endpoint, token o IP.

API e Avançado


7 cards
API mode
rails new api_app --api

class PostsController < ApplicationController
  def index
    render json: Post.all
  end

  def show
    render json: Post.find(params[:id])
  end
end

--api crea una app sin views, assets ni cookies. Los controllers heredan de ActionController::API (más ligero). render json: serializa objetos automáticamente.

Caching
# Fragment caching en la view:
<% cache @post do %>
  <%= render @post %>
<% end %>

# Low-level caching:
Rails.cache.fetch("populares", expires_in: 1.hour) do
  Post.order(views: :desc).limit(10)
end

cache en la view guarda HTML renderizado (se invalida al actualizar). Rails.cache.fetch guarda cualquier dato con expiración. Usa Redis como store en producción.

Serializers
gem "jsonapi-serializer"

class PostSerializer
  include JSONAPI::Serializer
  attributes :título, :texto
  belongs_to :autor
  attribute :url do |post|
    post_url(post)
  end
end

jsonapi-serializer formatea respuestas en el estándar JSON:API. attributes lista campos. attribute con bloque crea campos computados. Más rápido que to_json manual.

Action Cable (WebSockets)
rails g channel Notificaciones

class NotificacionesChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notificaciones"
  end
end

ActionCable.server.broadcast(
  "notificaciones", { msg: "Hola" })

Action Cable integra WebSockets en Rails. stream_from se suscribe a un canal. broadcast envía mensajes a todos los suscriptores. Ideal para notificaciones en tiempo real.

Active Job
rails g job EnviarEmail

class EnviarEmailJob < ApplicationJob
  queue_as :default

  def perform(user)
    UserMailer.bienvenido(user).deliver_now
  end
end

EnviarEmailJob.perform_later(user)

Active Job es la interfaz para background jobs. perform_later encola (no bloquea). queue_as define la cola. Usa Sidekiq o Solid Queue como backend.

Engines y concerns
# app/models/concerns/buscable.rb
module Buscable
  extend ActiveSupport::Concern

  included do
    scope :buscar, ->(q) {
      where("título LIKE ?", "%#{q}%") }
  end
end

class Post < ApplicationRecord
  include Buscable
end

Concerns son módulos reutilizables (mixins). ActiveSupport::Concern permite hooks included. Mantienen los models DRY. Engines son mini-apps empaquetables (gems internas).

Action Mailer
rails g mailer UserMailer

def bienvenido(user)
  @user = user
  mail(to: user.email,
    subject: "¡Bienvenido!")
end

# app/views/user_mailer/bienvenido.html.erb
UserMailer.bienvenido(user).deliver_later

Action Mailer envía emails con templates ERB. deliver_later envía vía background job. Configura SMTP en config/environments/production.rb o usa ActionMailer::Base.delivery_method.

Testes e Deploy


8 cards
Minitest (unit)
class PostTest < ActiveSupport::TestCase
  test "título es obligatorio" do
    post = Post.new(título: nil)
    assert_not post.valid?
    assert_includes post.errors[:título],
      "can't be blank"
  end
end

Minitest viene con Rails. test "..." define un caso. assert_not verifica falsedad. assert_includes confirma presencia en un array. Ejecuta con rails test.

System tests
class PostsSystemTest < ApplicationSystemTestCase
  test "crear post vía navegador" do
    visit new_post_path
    fill_in "Título", with: "Nuevo"
    click_button "Guardar"
    assert_text "¡Post creado!"
  end
end

Los system tests corren en un navegador real (Capybara). visit, fill_in, click_button simulan la interacción del usuario. Prueban el flujo completo (front + back).

Integration tests
class PostsFlowTest < ActionDispatch::IntegrationTest
  test "crear post" do
    post posts_path, params: {
      post: { título: "Nuevo" } }
    assert_response :redirect
    follow_redirect!
    assert_select "h1", "Nuevo"
  end
end

Los tests de integración simulan requests HTTP completos. post posts_path envía el request. assert_response verifica el status. assert_select hace assert sobre el HTML devuelto.

Preparar producción
RAILS_ENV=production rails db:migrate
rails assets:precompile
rails secret

# config/environments/production.rb
config.force_ssl = true
config.log_level = :info

assets:precompile compila CSS/JS con fingerprint. force_ssl fuerza HTTPS. rails secret genera secret_key_base. Logs en :info reducen ruido.

Fixtures y factories
# test/fixtures/posts.yml
primero:
  título: "Hola"
  texto: "Mundo"

# Con FactoryBot (gem):
FactoryBot.define do
  factory :post do
    título { "Prueba" }
  end
end

Fixtures son datos YAML estáticos. FactoryBot genera datos dinámicos con create(:post). Las factories son más flexibles y evitan acoplamiento entre tests.

Deploy (Capistrano)
gem "capistrano-rails"

# config/deploy.rb
set :application, "mi_app"
set :repo_url, "git@github.com:user/app.git"
set :deploy_to, "/var/www/app"

cap production deploy

Capistrano automatiza el deploy vía SSH. Hace pull del git, bundle install, migrations y restart. Soporta rollback con cap production deploy:rollback.

RSpec (alternativa)
RSpec.describe Post do
  it "requiere título" do
    post = Post.new(título: nil)
    expect(post).not_to be_valid
  end

  it "crea con título" do
    post = Post.create(título: "OK")
    expect(post).to be_persisted
  end
end

RSpec es la alternativa más popular a Minitest. Sintaxis describe/it/expect. Matchers como be_valid, eq, include hacen los tests legibles.

Logs y debugging
Rails.logger.info("Procesando...")
Rails.logger.error("Fallo: #{e.message}")

# En el controller/view:
debugger  # detiene la ejecución (gem debug)
logger.debug params.inspect

Rails.logger escribe en log/development.log. debugger detiene la ejecución para inspección interactiva. params.inspect muestra todos los parámetros recibidos.