web-dev-qa-db-fra.com

Comment tester ActionMailer delivery_later avec rspec

essayer de mettre à niveau vers Rails 4.2, en utilisant delay_job_active_record. Je n'ai pas défini le backend de delay_job pour l'environnement de test comme je pensais que les travaux s'exécuteraient immédiatement.

J'essaie de tester la nouvelle méthode "delivery_later" avec Rspec, mais je ne sais pas comment.

Ancien code du contrôleur:

ServiceMailer.delay.new_user(@user)

Nouveau code de contrôleur:

ServiceMailer.new_user(@user).deliver_later

J'AI UTILISÉ pour le tester comme ceci:

expect(ServiceMailer).to receive(:new_user).with(@user).and_return(double("mailer", :deliver => true))

Maintenant, je reçois des erreurs en utilisant cela. (Double "mailer" a reçu un message inattendu: delivery_later avec (pas d'arguments))

Juste

expect(ServiceMailer).to receive(:new_user)

échoue aussi avec la "méthode non définie" delivery_later "pour nil: NilClass"

J'ai essayé quelques exemples qui vous permettent de voir si les travaux sont mis en file d'attente à l'aide de test_helper dans ActiveJob, mais je n'ai pas réussi à tester que le travail correct est mis en file d'attente.

expect(enqueued_jobs.size).to eq(1)

Cela passe si le test_helper est inclus, mais cela ne me permet pas de vérifier que c'est le bon e-mail qui est envoyé.

Ce que je veux faire, c'est:

  • tester que l'e-mail correct est mis en file d'attente (ou exécuté immédiatement dans test env)
  • avec les bons paramètres (@user)

Des idées?? Merci

58
bobomoreno

Si je vous comprends bien, vous pourriez faire:

message_delivery = instance_double(ActionMailer::MessageDelivery)
expect(ServiceMailer).to receive(:new_user).with(@user).and_return(message_delivery)
allow(message_delivery).to receive(:deliver_later)

L'essentiel est que vous devez en quelque sorte fournir un double pour deliver_later.

69
Peter Alfvin

Si vous trouvez cette question mais utilisez ActiveJob plutôt que simplement DelayedJob seul, et utilisez Rails 5, je recommande de configurer ActionMailer dans config/environments/test.rb:

config.active_job.queue_adapter = :inline

(c'était le comportement par défaut avant Rails 5)

33
Gabe Kopley

En utilisant ActiveJob et rspec 3.4+, vous pouvez utiliser have_enqueued_job comme ceci:

expect {
  YourMailer.your_method.deliver_later
}.to have_enqueued_job.on_queue('mailers')
32
Alter Lagos

J'ajouterai ma réponse car aucune des autres n'était assez bonne pour moi:

1) Il n'est pas nécessaire de se moquer du Mailer: Rails le fait déjà pour vous.

2) Il n'est pas nécessaire de vraiment déclencher la création de l'email: cela prendra du temps et ralentira votre test!

Voilà pourquoi dans environments/test.rb vous devez définir les options suivantes:

config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test

Encore une fois: ne remettez pas vos e-mails à l'aide de deliver_now mais toujours utilisez deliver_later. Cela empêche vos utilisateurs d'attendre la livraison efficace de l'e-mail. Si vous n'avez pas sidekiq, sucker_punch, ou tout autre en production, utilisez simplement config.active_job.queue_adapter = :async. Et soit async ou inline pour l'environnement de développement.

Étant donné la configuration suivante pour l'environnement de test, vos e-mails seront toujours mis en file d'attente et ne seront jamais exécutés pour la livraison: cela vous empêche de vous moquer d'eux et vous pouvez vérifier qu'ils sont correctement mis en file d'attente.

Dans vos tests, toujours divisez le test en deux: 1) Un test unitaire pour vérifier que l'e-mail est correctement mis en file d'attente et avec les paramètres corrects 2) Un test unitaire pour le courrier pour vérifier que le sujet, l'expéditeur, le destinataire et le contenu sont corrects.

Étant donné le scénario suivant:

class User
  after_update :send_email

  def send_email
    ReportMailer.update_mail(id).deliver_later
  end
end

Écrivez un test pour vérifier que l'e-mail est correctement mis en file d'attente:

include ActiveJob::TestHelper
expect { user.update(name: 'Hello') }.to have_enqueued_job(ActionMailer::DeliveryJob).with('ReportMailer', 'update_mail', 'deliver_now', user.id)

et rédigez un test distinct pour votre e-mail

Rspec.describe ReportMailer do
    describe '#update_email' do
      subject(:mailer) { described_class.update_email(user.id) }
      it { expect(mailer.subject).to eq 'whatever' }
      ...
    end
end
  • Vous avez vérifié exactement que votre e-mail a été mis en file d'attente et non un travail générique.
  • Votre test est rapide
  • Tu n'avais besoin d'aucune moquerie

Lorsque vous écrivez un test système, n'hésitez pas à décider si vous souhaitez vraiment y envoyer des e-mails, car la vitesse n'a plus beaucoup d'importance. Personnellement, j'aime configurer les éléments suivants:

RSpec.configure do |config|
  config.around(:each, :mailer) do |example|
    perform_enqueued_jobs do
      example.run
    end
  end
end

et attribuez le :mailer attribut aux tests où je veux réellement envoyer des e-mails.

Pour en savoir plus sur la configuration correcte de votre e-mail dans Rails lisez cet article: https://medium.com/@coorasse/the-correct-emails-configuration-in-Rails -c1d8418c0bfd

12
coorasse

Ajoute ça:

# spec/support/message_delivery.rb
class ActionMailer::MessageDelivery
  def deliver_later
    deliver_now
  end
end

Référence: http://mrlab.sk/testing-email-delivery-with-deliver-later.html

8
Minimul

Une meilleure solution (que le monkeypatching deliver_later) Est:

require 'spec_helper'
include ActiveJob::TestHelper

describe YourObject do
  around { |example| perform_enqueued_jobs(&example) }

  it "sends an email" do
    expect { something_that.sends_an_email }.to change(ActionMailer::Base.deliveries, :length)
  end
end

La fonction around { |example| perform_enqueued_jobs(&example) } garantit l'exécution des tâches d'arrière-plan avant de vérifier les valeurs de test.

7
Qqwy

Je suis venu avec le même doute et j'ai résolu d'une manière moins verbeuse (une seule ligne) inspirée par cette réponse

expect(ServiceMailer).to receive_message_chain(:new_user, :deliver_later).with(@user).with(no_args)

Notez que la dernière with(no_args) est essentielle.

Mais, si cela ne vous dérange pas si deliver_later Est appelé, faites simplement:

expect(ServiceMailer).to expect(:new_user).with(@user).and_call_original

3
Luccas

Un moyen simple est:

expect(ServiceMailer).to(
  receive(:new_user).with(@user).and_call_original
)
# subject
2
Nuno Silva

Cette réponse est un peu différente, mais peut aider dans des cas comme un nouveau changement dans l'API Rails, ou un changement dans la façon dont vous voulez livrer (comme utiliser deliver_now au lieu de deliver_later).

Ce que je fais la plupart du temps, c'est de passer un mailer en tant que dépendance à la méthode que je teste, mais je ne passe pas un mailer depuis Rails, je passe plutôt un objet qui fera les choses de la manière Je voudrais"...

Par exemple si je veux vérifier que j'envoie le bon mail après l'enregistrement d'un utilisateur ... je pourrais le faire ...

class DummyMailer
  def self.send_welcome_message(user)
  end
end

it "sends a welcome email" do
  allow(store).to receive(:create).and_return(user)
  expect(mailer).to receive(:send_welcome_message).with(user)
  register_user(params, store, mailer)
end

Et puis dans le contrôleur où j'appellerai cette méthode, j'écrirais la "vraie" implémentation de ce mailer ...

class RegistrationsController < ApplicationController
  def create
    Registrations.register_user(params[:user], User, Mailer)
    # ...
  end

  class Mailer
    def self.send_welcome_message(user)
      ServiceMailer.new_user(user).deliver_later
    end
  end
end

De cette façon, je sens que je teste que j'envoie le bon message, au bon objet, avec les bonnes données (arguments). Et j'ai juste besoin de créer un objet très simple qui n'a pas de logique, juste la responsabilité de savoir comment ActionMailer veut être appelé.

Je préfère le faire parce que je préfère avoir plus de contrôle sur les dépendances que j'ai. Il s'agit d'un exemple du "Principe d'inversion des dépendances" .

Je ne sais pas si c'est votre goût, mais c'est une autre façon de résoudre le problème =).

0
Benito Serna

Je suis venu ici à la recherche d'une réponse pour un test complet, donc pas juste demandant s'il y a un mail en attente d'envoi, en plus, pour son destinataire, sujet ... etc

J'ai une solution, qui vient de ici , mais avec un petit changement:

Comme il est dit, la partie curiale est

mail = perform_enqueued_jobs { ActionMailer::DeliveryJob.perform_now(*enqueued_jobs.first[:args]) }

Le problème est que les paramètres que mailer reçoit, dans ce cas, sont différents des paramètres que reçoit en production, en production, si le premier paramètre est un modèle, maintenant en test recevra un hachage, donc plantera

enqueued_jobs.first[:args]
["UserMailer", "welcome_email", "deliver_now", {"_aj_globalid"=>"gid://forjartistica/User/1"}]

Donc, si nous appelons l'expéditeur comme UserMailer.welcome_email(@user).deliver_later l'expéditeur reçoit en production un utilisateur, mais en test recevra {"_aj_globalid"=>"gid://forjartistica/User/1"}

Tous les commentaires seront appréciés, La solution la moins douloureuse que j'ai trouvée est de changer la façon dont j'appelle les expéditeurs, en passant, l'ID du modèle et non le modèle:

UserMailer.welcome_email(@user.id).deliver_later

0
Albert Català