¿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.