Testing en Ruby: Minitest vs RSpec

Lucas Hourquebie
Unagi
6 min readNov 2, 2018

--

El testing es una parte imprescindible en el proceso de desarrollo de software y nos permite validar el funcionamiento de un sistema conforme a sus requerimientos y reglas de negocio, constituyendo un paso necesario para asegurar la calidad de los sistemas.

Por lo tanto, el testing sirve, por un lado, para garantizar el correcto funcionamiento de los sistemas que desarrollamos, y por otro para validar que los cambios venideros que pueden surgir con el paso del tiempo no generen efectos secundarios tales como bugs o flujos de funcionamiento incorrecto.

Casi como una ley natural, los sistemas informáticos evolucionan y se adaptan constantemente a nuevos cambios.

Más allá de lo dicho, el testing también es vital para documentar el funcionamiento del software. Partiendo de la base de que escribimos tests para validar los requerimientos de un sistema, sus incumbencias, limitaciones y reglas de negocios, podemos concluir en que también constituyen un documento que indica lo que un sistema puede y debe hacer, así como aquello que no.

Todo lenguaje de programación está dotado de diferentes herramientas para testear aplicaciones, y Ruby no escapa de esa realidad: existen diversos frameworks de testing ampliamente utilizados, entre los que destacan RSpec y Minitest.

Por un lado, RSpec es el framework de testing más utilizado y aclamado por la comunidad de desarrolladores Ruby, mientras que Minitest es la herramienta de testing de facto de Ruby on Rails, uno de los frameworks de desarrollo web más populares de la actualidad.

En Unagi utilizamos ambos frameworks, por lo que vamos a enumerar sus características basadas en nuestras experiencias y, después, te dejamos como tarea elegir cuál de los dos te convence más.

Minitest

Muchos de nuestros sistemas están construidos en Ruby on Rails. Por lo tanto, inicialmente, tomamos la decisión de utilizar la herramienta que viene por defecto. Minitest es muy simple de utilizar, tiene una sintaxis sencilla y es de rápida implementación con tests de baja a mediana complejidad.

Su utilización consiste básicamente en definir clases que se encarguen de testear a otra clase Ruby, o bien a una integración de ellas, dependiendo del tipo de test que se quiere realizar. Cada clase se compone básicamente de tres partes:

  • test: Operaciones básicas que se ejecutan para verificar algún requerimiento en particular. Su definición requiere de un string que define aquello que se está testeando.
  • setup: Bloque de código que se ejecuta antes de correr cada test, una vez por cada uno de ellos, para configurar un entorno común.
  • teardown: Bloque de código que se ejecuta luego de correr cada test, una vez por cada uno de ellos. Se utiliza para realizar una limpieza post-test.

En el siguiente ejemplo podemos observar el test de una clase que maneja la autenticación de un usuario:

class ApiLoginManager < Minitest::Test
setup do
@user = FactoryBot.create(:user)
end
test 'should update user auth_token on successful login' do
assert_changes -> { @user.reload.auth_token} do
response = ApiLoginManager
.new(email: @user.email, password: 'supersecret')
.call
end
end
test 'should return the auth_token on successful login' do
response = ApiLoginManager
.new(email: @user.email, password: 'supersecret')
.call
assert_equal response, @user.reload.auth_token
end
test 'should return EMPTY_EMAIL error when no email is provided' do
service = ApiLoginManager.new(password: 'supersecret')
refute service.call
assert_equal ApiLoginManager::EMPTY_EMAIL, service.error
end
test 'should return EMPTY_PASSWORD error when no password is provided' do
service = ApiLoginManager.new(email: @user.email)
refute service.call
assert_equal ApiLoginManager::EMPTY_PASSWORD, service.error
end
test 'should return USER_NOT_FOUND error when email is incorrect' do
service = ApiLoginManager.new(
email: 'incorrect.email@mail.com',
password: 'supersecret'
)
refute service.call
assert_equal ApiLoginManager::USER_NOT_FOUND, service.error
end
test 'should return USER_NOT_FOUND error when password is incorrect' do
service = ApiLoginManager.new(
email: @user.email,
password: 'incorrect_password'
)
refute service.call
assert_equal ApiLoginManager::USER_NOT_FOUND, service.error
end
end

Cada vez que definimos un bloque test validamos algún requerimiento o funcionalidad, y lo hacemos mediante assertions (o refutations), los cuales validan un resultado en particular, de forma tal de que si las condiciones no se cumple el test falla. Podemos definir varios en cada test y la falla de uno hace fallar al test completo. Existen numerosos tipos de assertions/refutations y pueden encontrarse en este enlace.

RSpec

Últimamente hemos estado cambiando nuestras herramientas de testing y encontramos en RSpec un gran aliado, con una integración perfecta con Ruby on Rails, potenciado por la ayuda de herramientas de las que hablaremos en otra publicación: FactoryBot, Shoulda Matchers y Faker.

RSpec es, a juzgar por las gemas y proyectos que hemos visto, el framework preferido por los desarrolladores Ruby. Es una herramienta muy robusta y preparada para testear cualquier solución que le haga frente.

RSpec es uno de los frameworks de testing más utilizados en la comunidad Ruby.

La estructura de los tests escritos con RSpec es más flexible que la de Minitest, y permite utilizar diferentes descripciones y contextos para clarificar las incumbencias de aquello que se está testeando. A continuación podemos observar un ejemplo utilizando la misma clase que se usó en el ejemplo de Minitest:

RSpec.describe ApiLoginManager do
let(:user) { FactoryBot.create(:user) }

describe 'Successful login' do
subject do
ApiLoginManager.new(email: user.email, password: 'supersecret').call
end
it 'should update user auth_token' do
expect { subject }.to change { user.reload.auth_token }
end
it 'should return the auth_token' do
auth_token = subject
expect(auth_token).to eq(user.reload.auth_token)
end
end
describe 'Failed login' do
context 'when no email is provided' do
it 'should return EMPTY_EMAIL error' do
service = ApiLoginManager.new(password: 'supersecret')
expect(service.call).to be false
expect(service.error).to eq(ApiLoginManager::EMPTY_EMAIL)
end
end
context 'when no password is provided' do
it 'return EMPTY_PASSWORD error' do
service = ApiLoginManager.new(email: user.email)
expect(service.call).to be false
expect(service.error).to eq(ApiLoginManager::EMPTY_PASSWORD)
end
end
context 'when the email is incorrect' do
it 'should return USER_NOT_FOUND error' do
service = ApiLoginManager.new(email: 'a@mail.com', password: 'test')
expect(service.call).to be false
expect(service.error).to eq(ApiLoginManager::USER_NOT_FOUND)
end
end
context 'when the password is incorrect' do
it 'should return WRONG_PASSWORD error' do
service = ApiLoginManager.new(email: user.email, password: 'test')
expect(service.call).to be false
expect(service.error).to eq(ApiLoginManager::WRONG_PASSWORD)
end
end
end
end

En el ejemplo se pueden observar varios elementos propios de RSpec:

  • describe: Indica un bloque de ejecución de tests mediante una descripción de lo que se va a testear, lo cual puede ser una clase, un método o una acción. Puede pensarse como una colección de tests.
  • context: Indica un bloque con el contexto donde se van a ejecutar los tests. Generalmente se describe como un escenario con condiciones en donde una funcionalidad se puede ejecutar. Al escribir un contexto, su descripción comienza generalmente con “cuando” (when) o “con” (with). Puede pensarse como un subconjunto de tests de una misma particularidad dentro de un describe.
  • it: Describe el test particular que se va a ejecutar, es decir, el test case.
  • expect: Es análogo a un assertion de Minitest, y es el encargado de verificar un resultado, haciendo fallar el test si el resultado esperado difiere del obtenido.
  • subject: Describe un sujeto de pruebas, es decir, la acción principal sobre la que recae un test. Su uso es opcional y es recomendable cuando un conjunto de tests utilizan un bloque de código común que va a generar acciones cuyos resultados se quieren testear.
  • let: Se utiliza cuando se tiene que asignar una variable, y su definición tiene alcance en el bloque en donde se haya declarado. Con let la variable se carga sólo cuando es usada por primera vez en el test y se mantiene en caché hasta que la prueba específica termina. Tiene una variante let! que se utiliza para definir la variable cuando el bloque es definido.

Creemos que el testing es un proceso necesario, y es por eso que en Unagi forma parte del ciclo regular del desarrollo de toda aplicación. Entendemos que es de vital importancia elegir buenas herramientas de testing que ayuden a mejorar la calidad de las soluciones que desarrollamos. Es por ese motivo que actualmente utilizamos RSpec ya que, en nuestra opinión y basado en nuestras experiencias, consideramos algunos puntos positivos que respaldan la decisión:

  • Genera tests con gran legibilidad que acompañan la filosofía de Ruby en lo que respecta a expresividad del lenguaje.
  • Presenta un mayor soporte a nivel global por parte de la comunidad de desarrolladores Ruby.
  • Permite definir múltiples contextos para separar incumbencias, permitiendo declarar hooks y variables en cada contexto en particular.

Y vos, ¿qué framework de testing utilizás para tus aplicaciones Ruby/Ruby on Rails? Dejanos tu comentario y algunos 👏 si te gustó el artículo.

Unagi (unagi.com.ar) es una empresa de software enfocada en el desarrollo de soluciones web que ayuden a nuestros clientes a mejorar lo que ya hacen bien. Nuestro equipo está formado por un grupo de ingenieros y licenciados con más de 10 años de experiencia acumulada. Estamos abiertos a nuevas experiencias, y nos encantan los desafíos. Somos felices haciendo lo que hacemos.

--

--

Lucas Hourquebie
Unagi
Writer for

Software engineer, Jedi Knight and Pokémon trainer. He/him.