Cómo evitar introducir errores aleatorios en specs ruby que esperan el envío de un mail.

By

¿Tus specs funcionaban perfecto...hasta que comenzaron a fallar aleatoriamente? A quién no le ha pasado. Acá te contamos cómo evitarlo.

A menudo, escribimos specs para cubrir el código que envía un correo como parte de su ejecución, pero cuando el código cambia y se agrega otro correo, estos specs pueden fallar de manera aleatoria. Vamos a mostrar cómo usar rspec mocks para imitar las clases y objetos reales del correo y asegurarnos de que el código los invoca de la manera correcta.

Escenario

Usualmente escribimos specs para cubrir código que envía un correo como parte de su ejecución (ej. correo welcome):

it "creates the user account" do
  ...
  mail = ActionMailer::Base.deliveries.first
  expect(mail.subject).to match("Welcome to Get on Board")
  ...
end

Esto funciona un rato, hasta que cambia el código, que digamos ahora además agrega otro correo (por ejemplo notificando a los administradores que una nueva cuenta se agregó) y que hace que este spec falle en forma aleatoria, pues el correo de welcome ya no es necesariamente el primero en deliveries.

Parche

it "creates the user account" do
  ...
  mails = ActionMailer::Base.deliveries
  expect(mails.map(&:subject)).to include("Welcome to Get on Board")
  ...
end

Este parche arregla el spec, pero es feo y tal como la solución anterior prueba el contenido del correo (que debería estar en su propio spec del mailer) en vez probar que el code detone el correo de welcome.

La solución

Usar rspec mocks para imitar las clases y objetos reales del correo. Acá no nos interesa probar el contenido del correo, sino solo estar seguros que el código lo invoca.

let(:mail) { double } # mock object
before do
  # Add deliver_later method to the mock
  allow(mail).to receive(:deliver_later).with(no_args)
  # Create a mock for the mailer class add the mock method welcome_user
  # that also return the mock object mail
  allow(UserMailer).to receive(:welcome_user).and_return(mail)
  allow(AdminMailer).to receive(:new_account).and_return(mail)
end

it "creates the user account" do
  ...
  # Verify that creating an user account involves sending these emails
  # with the expected parameters and only once
  expect(UserMailer).to receive(:welcome_user).with(user.id).once
  expect(AdminMailer).to receive(:new_account).with(user.name).once

  # Verify that such mails are delivered asynchronously
  expect(mail).to receive(:perform_later).twice
  ...
end

Esta solución no solo es más elegante, además prueba que  los correos reciban los parámetros esperados y sean detonados solo una vez y en forma asíncrona.

Nota: Este tip (que no solo aplica al envío de correos), es parte de las buenas prácticas de escribir tests unitarios, que se limiten a probar la lógica principal del código que queremos cubrir dejando los detalles de implementación de otras capas (como el mailing) de la aplicación para otros tests (por ejemplo en este caso particular el contenido de los correos pudiera estar en ser specs/mailers/user_mailer_spec.rb y specs/mailers/admin_mailer_spec.rb).

En conclusión...

Usar rspec mocks como solución no solo es más elegante, sino que también prueba que los correos reciben los parámetros esperados y son detonados solo una vez y en forma asíncrona. Es importante recordar que esta es solo una de las buenas prácticas de escribir tests unitarios y que debemos dejar los detalles de implementación de otras capas de la aplicación para otros tests.

Latest on Blog