Matchers comunes en RSpec
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/
RSpec es uno de los frameworks de pruebas más populares en el ecosistema Ruby, y una de sus fortalezas más destacadas es la expresividad de sus matchers: construcciones que permiten definir con claridad y precisión lo que esperamos que ocurra en el código bajo prueba.
En esta lección, revisaremos los matchers más comunes que ofrece RSpec, cómo se utilizan para construir expectativas legibles y directas, y qué diferencias existen con otros frameworks como Kotest en Kotlin. También discutiremos sus ventajas y limitaciones desde la perspectiva del diseño de bibliotecas y herramientas reutilizables.
Ejemplos Comunes de Matchers en RSpec
Igualdad y Desigualdad con eq
El matcher eq
en RSpec verifica si dos valores son iguales estructuralmente. Esto significa que compara los contenidos mediante el método ==
, no su identidad de objeto (equal?
). Por lo tanto, dos objetos distintos pueden ser considerados iguales si su contenido lo es.
💡 Ejemplo contextualizado
Supongamos que estás desarrollando una biblioteca para manejar configuraciones de usuario. Una de las funciones devuelve un objeto con la configuración por defecto. Queremos asegurarnos de que esa función construya el contenido correctamente, sin preocuparnos por si es la misma instancia:
- Código esencial
- Código completo
expect(default_config).to eq(UserConfig.new)
require_relative 'user_config'
RSpec.describe "Default configuration" do
it "returns a config with 'light' theme and notifications enabled" do
expect(default_config).to eq(UserConfig.new)
end
end
class UserConfig
attr_reader :theme, :notifications_enabled
def initialize(theme: "light", notifications_enabled: true)
@theme = theme
@notifications_enabled = notifications_enabled
end
def ==(other)
other.is_a?(UserConfig) &&
other.theme == theme &&
other.notifications_enabled == notifications_enabled
end
end
def default_config
UserConfig.new
end
Esta prueba verifica que default_config
devuelva una instancia con los valores por defecto esperados. Aunque se trata de dos objetos distintos, la prueba pasa porque eq
utiliza el método ==
, y en este caso hemos definido que dos configuraciones son iguales si tienen el mismo theme
y notifications_enabled
.
Esto es especialmente útil al diseñar bibliotecas, ya que permite verificar el contenido esperado sin acoplarse a la identidad exacta de la instancia.
❌ Desigualdad
Si lo que deseas es comprobar que dos valores no son iguales estructuralmente, puedes usar el matcher not_to
(o to_not
):
expect(test_config).not_to eq(default_config)
Esto asegura que, aunque ambos objetos sean del mismo tipo, sus contenidos no son equivalentes según la definición de ==
.
expect(x).to eq(y)
→ verifica quex == y
expect(x).not_to eq(y)
→ verifica quex != y
Ambas formas respetan el método ==
definido en la clase. Esto significa que puedes adaptar qué se considera "igual" para cada tipo de objeto según tus necesidades.
Si necesitas comprobar que dos objetos son exactamente la misma instancia (es decir, ocupan el mismo lugar en memoria), puedes usar:
expect(a).to equal(b)
Este matcher no usa ==
, sino equal?
, que compara identidad.
Nulidad
En RSpec, puedes verificar si un valor es nil
(nulo) usando los matchers be_nil
y not_to be_nil
.
Esto es especialmente útil al construir bibliotecas que retornan valores opcionales o que permiten estados vacíos como parte de su diseño. Asegurar la presencia o ausencia de un valor es una parte fundamental del control de flujo y la validación de resultados.
💡 Ejemplo contextualizado
Supongamos que estás desarrollando una biblioteca que consulta configuraciones opcionales de usuario. Si no existe una configuración específica, tu función debe retornar nil
:
- Código esencial
- Código completo
expect(get_config("color_theme")).to be_nil
require_relative 'user_settings'
RSpec.describe UserSettings do
it "returns nil for unknown configuration keys" do
settings = UserSettings.new
expect(settings.get_config("color_theme")).to be_nil
end
end
class UserSettings
def initialize(data = {})
@data = data
end
def get_config(key)
@data[key]
end
end
Este test verifica que la función get_config
retorna nil
cuando se consulta una clave que no ha sido definida. Esto es útil para APIs que permiten valores opcionales, y nos ayuda a validar su comportamiento en escenarios comunes como configuraciones faltantes, respuestas vacías, o estados iniciales.
✅ Para verificar que un valor no sea nulo
También puedes validar explícitamente que un resultado no sea nil
usando not_to be_nil
:
expect(user.name).not_to be_nil
Esto es útil en validaciones donde se espera que el resultado esté siempre presente, por ejemplo, después de una carga exitosa o una conversión válida.
be_nil
verifica si un valor es exactamentenil
.not_to be_nil
comprueba que existe algún valor distinto denil
—sin importar cuál.
Contenido en cadenas
RSpec ofrece varios matchers para trabajar con cadenas de texto, permitiendo verificar desde contenido parcial hasta coincidencias más específicas como prefijos o sufijos. Esto es muy útil en bibliotecas que generan mensajes, encabezados, logs o formatos estructurados.
🔍 Matchers disponibles
-
include(substring)
Verifica que la cadena contenga una subcadena específica. -
start_with(prefix)
Verifica que la cadena comience con un prefijo determinado. -
end_with(suffix)
Verifica que la cadena termine con un sufijo específico.
💡 Ejemplo contextualizado
Supongamos que estás desarrollando una biblioteca que genera mensajes de log. Cada mensaje debe comenzar con un nivel de severidad ([INFO]
, [WARN]
, etc.), incluir un texto principal, y terminar con un identificador de evento:
- Código esencial
- Código completo
expect(log).to start_with("[WARN]")
expect(log).to include("Bloody New Year's Eve")
expect(log).to end_with("#friend-001")
def generate_log(message, level = "INFO", event_id = nil)
"[#{level}] #{message}#{event_id ? " ##{event_id}" : ""}"
end
require_relative 'log_generator'
RSpec.describe "Log format" do
it "includes level, message and event ID" do
log = generate_log("Bloody New Year's Eve was triggered", "WARN", "friend-001")
expect(log).to start_with("[WARN]")
expect(log).to include("Bloody New Year's Eve")
expect(log).to end_with("#friend-001")
end
end
Este test valida que un mensaje de log generado por la biblioteca incluya tres elementos fundamentales:
- Un nivel de alerta (
[WARN]
), necesario para categorizar la gravedad del evento. - Una descripción textual del evento (
"Bloody New Year's Eve"
). - Un identificador de evento (
#friend-001
).
Este tipo de test sería útil en una biblioteca que formatea registros de auditoría o trazabilidad de eventos críticos en sistemas complejos.
🧬 Matchers de tipo y clase
En RSpec, puedes verificar si un objeto es de cierto tipo o clase usando los matchers be_a
, be_an
, be_instance_of
y be_kind_of
. Esto es especialmente útil cuando estás desarrollando una biblioteca y quieres asegurarte de que tus funciones devuelvan objetos del tipo correcto o compatible.
be_a
/ be_an
Verifican si un objeto es una instancia de una clase o de una subclase.
expect(user).to be_a(User)
expect(token).to be_an(String)
be_instance_of
Verifica si un objeto es exactamente de una clase, sin permitir subclases.
expect(user).to be_instance_of(User) # Falla si `user` es de una subclase como `AdminUser`
be_kind_of
Alias de be_a
/ be_an
, útil cuando se trabaja con jerarquías de clases o módulos incluidos.
📌 Ejemplo
Supongamos que estás desarrollando una biblioteca para procesar comandos CLI. Cada comando debe devolver un resultado que implementa una interfaz común (CommandResult
), pero también puedes tener subtipos para representar distintos casos.
- Código esencial
- Código completo
result = CommandProcessor.run("version")
expect(result).to be_a(CommandResult)
expect(result).to be_instance_of(SuccessResult)
require_relative 'command_processor'
RSpec.describe CommandProcessor do
it "returns a success result that is a kind of CommandResult" do
result = CommandProcessor.run("version")
expect(result).to be_a(CommandResult)
expect(result).to be_instance_of(SuccessResult)
end
end
# Define the shared interface
class CommandResult
attr_reader :message
def initialize(message)
@message = message
end
end
# Subtipo para resultados exitosos
class SuccessResult < CommandResult
end
# Procesador de comandos
module CommandProcessor
def self.run(command)
case command
when "version"
SuccessResult.new("Library CLI v1.2.0")
else
raise "Unknown command"
end
end
end
Este test verifica dos cosas:
- Que
run("version")
devuelve un objeto compatible conCommandResult
(be_a
) — útil si otras clases heredan de él. - Que el tipo exacto del resultado sea
SuccessResult
(be_instance_of
) — útil para distinguir variantes específicas de resultado.
Este enfoque es común en bibliotecas que definen jerarquías de tipos para representar distintos estados o resultados, y permite mantener un diseño extensible sin sacrificar la precisión de las pruebas.