So testen Sie ActionMailer Deliver_Later mit rspec

75

Versuch, mit delay_job_active_record ein Upgrade auf Rails 4.2 durchzuführen. Ich habe das verzögerte_job-Backend für die Testumgebung nicht so festgelegt, dass Jobs sofort ausgeführt werden.

Ich versuche, die neue Methode 'Deliver_Later' mit Rspec zu testen, bin mir aber nicht sicher, wie.

Alter Controller-Code:

ServiceMailer.delay.new_user(@user)

Neuer Controller-Code:

ServiceMailer.new_user(@user).deliver_later

Ich habe es so getestet:

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

Jetzt bekomme ich Fehler damit. (Doppelter "Mailer" hat unerwartete Nachricht erhalten: Deliver_Later mit (keine Argumente))

Gerade

expect(ServiceMailer).to receive(:new_user)

schlägt auch mit 'undefinierter Methode' liefern_later 'für nil: NilClass' fehl

Ich habe einige Beispiele ausprobiert, mit denen Sie mithilfe von test_helper in ActiveJob feststellen können, ob Jobs in die Warteschlange gestellt wurden. Es ist mir jedoch nicht gelungen, zu testen, ob der richtige Job in der Warteschlange steht.

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

Dies ist erfolgreich, wenn test_helper enthalten ist, aber ich kann nicht überprüfen, ob es sich um die richtige E-Mail handelt, die gesendet wird.

Was ich tun möchte ist:

  • Testen Sie, ob die richtige E-Mail in der Warteschlange steht (oder sofort in test env ausgeführt wird).
  • mit den richtigen Parametern (@user)

Irgendwelche Ideen?? Vielen Dank

Bobomoreno
quelle

Antworten:

80

Wenn ich Sie richtig verstehe, können Sie Folgendes tun:

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)

Der Schlüssel ist, dass Sie irgendwie ein Doppel für bereitstellen müssen deliver_later.

Peter Alfvin
quelle
Sollte das nicht sein allow(message_delivery).to …? Schließlich testen Sie das Ergebnis bereits durch Erwartung new_user.
Morgler
1
@morgler Einverstanden. Ich habe die Antwort aktualisiert. Vielen Dank für das Bemerken / Kommentieren.
Peter Alfvin
1
Dies mag ein wenig vom Thema @morgler abweichen, aber ich bin gespannt, was Sie oder andere im Allgemeinen denken, wenn Sie aus irgendeinem Grund (wie aus Versehen) sagen, dass die deliver_laterMethode aus dem Controller entfernt wird, indem allowwir sie nicht verwenden können das richtig verstehen? Ich meine, der Test wird noch bestehen. Denken Sie immer noch, dass die Verwendung eines allowWillens eine bessere Idee ist als die Verwendung expect? Ich habe diese expectFlaggen gesehen, wenn sie versehentlich deliver_laterentfernt wurden und deshalb wollte ich das im Allgemeinen diskutieren. Es wäre großartig, wenn Sie näher erläutern könnten, warum allowder obige Kontext besser ist.
Boddhisattva
@ Boddhisattva ein gültiger Punkt. Diese Spezifikation soll jedoch testen, ob die Methode von ServiceMailer' new_useraufgerufen wird. Sie können einen weiteren Test erstellen, der deliver_laterdie aufgerufene Methode testet , sobald die E-Mail erstellt wurde.
Morgler
@morgler Danke für deine Antwort auf meine Frage. Ich verstehe jetzt, dass Sie hauptsächlich die Verwendung von allowbasierend auf dem Kontext der Testmethode vorgeschlagen haben ServiceMailer's new_user. Für den Fall, dass ich testen müsste deliver_later, dachte ich, ich würde dem vorhandenen Test (der nach ServiceMailer's new_userMethoden sucht) einfach eine weitere Aussage hinzufügen , um so etwas zu überprüfen, expect(mailer_object).to receive(:deliver_later)anstatt dies als einen weiteren Test insgesamt zu testen. Es wäre interessant zu wissen, warum Sie einen separaten Test dafür bevorzugen, falls wir jemals testen müssten deliver_later.
Boddhisattva
44

Mit ActiveJob und rspec-rails3.4+ können Sie Folgendes verwenden have_enqueued_job:

expect { 
  YourMailer.your_method.deliver_later 
  # or any other method that eventually would trigger mail enqueuing
}.to( 
  have_enqueued_job.on_queue('mailers').with(
    # `with` isn't mandatory, but it will help if you want to make sure is
    # the correct enqueued mail.
    'YourMailer', 'your_method', 'deliver_now', any_param_you_want_to_check
  )
)

Sie können auch doppelt einchecken config/environments/test.rb:

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

Eine andere Möglichkeit wäre, Inline-Jobs auszuführen:

config.active_job.queue_adapter = :inline

Beachten Sie jedoch, dass dies die Gesamtleistung Ihrer Testsuite beeinträchtigen würde , da alle Ihre Jobs ausgeführt werden, sobald sie in die Warteschlange gestellt werden.

Alter Lagos
quelle
36

Wenn Sie diese Frage finden, aber ActiveJob anstelle von DelayedJob verwenden und Rails 5 verwenden, empfehle ich, ActionMailer wie folgt zu konfigurieren config/environments/test.rb:

config.active_job.queue_adapter = :inline

(Dies war das Standardverhalten vor Rails 5)

Gabe Kopley
quelle
Würde es nicht alle asynchronen Aufgaben ausführen, während Spezifikationen ausgeführt werden?
Aleksey
Ja, das wäre ein guter Punkt. Dies ist praktisch in einfachen und einfachen Anwendungsfällen von ActiveJob, in denen Sie alle asynchronen Aufgaben für die Inline-Ausführung konfigurieren können, und vereinfacht das Testen.
Gabe Kopley
1
Wahrscheinlich habe ich mir gerade eine Stunde Debugging erspart. Vielen Dank!
Matt
Dies funktionierte früher wunderbar, scheint aber in einem kürzlich veröffentlichten Bundle-Update aufgehört zu haben :(
Irgendwelche
27

Ich werde meine Antwort hinzufügen, weil keiner der anderen gut genug für mich war:

1) Es ist nicht nötig, den Mailer zu verspotten: Rails erledigt das im Grunde schon für Sie.

2) Es ist nicht notwendig, die Erstellung der E-Mail wirklich auszulösen: Dies kostet Zeit und verlangsamt Ihren Test!

Aus diesem Grund sollten in environments/test.rbIhnen die folgenden Optionen festgelegt sein:

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

Nochmals: nicht liefern Ihre E - Mails mit deliver_nowaber immer verwenden deliver_later. Dies verhindert, dass Ihre Benutzer auf die effektive Zustellung der E-Mail warten. Wenn Sie nicht haben sidekiq, sucker_punchoder jede andere in der Produktion, einfach zu verwenden config.active_job.queue_adapter = :async. Und entweder asyncoder inlinefür die Entwicklungsumgebung.

Bei der folgenden Konfiguration für die Testumgebung werden Ihre E-Mails immer in die Warteschlange gestellt und niemals zur Zustellung ausgeführt: Dies verhindert, dass Sie sie verspotten, und Sie können überprüfen, ob sie korrekt in die Warteschlange gestellt wurden.

Teilen Sie den Test in Ihren Tests immer in zwei Teile auf: 1) Ein Einheitentest, um zu überprüfen, ob die E-Mail korrekt in die Warteschlange gestellt wurde und mit den richtigen Parametern. 2) Ein Einheitentest für die E-Mail, um zu überprüfen, ob Betreff, Absender, Empfänger und Inhalt korrekt sind .

Angesichts des folgenden Szenarios:

class User
  after_update :send_email

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

Schreiben Sie einen Test, um zu überprüfen, ob die E-Mail korrekt in die Warteschlange gestellt wurde:

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

und schreiben Sie einen separaten Test für Ihre 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
  • Sie haben genau getestet, dass Ihre E-Mail in die Warteschlange gestellt wurde und kein generischer Job ist.
  • Ihr Test ist schnell
  • Du brauchst keine Verspottung

Wenn Sie einen Systemtest schreiben, können Sie frei entscheiden, ob Sie dort wirklich E-Mails versenden möchten, da die Geschwindigkeit nicht mehr so ​​wichtig ist. Ich persönlich möchte Folgendes konfigurieren:

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

und weisen Sie das :mailerAttribut den Tests zu, bei denen ich tatsächlich E-Mails senden möchte.

Weitere Informationen zum korrekten Konfigurieren Ihrer E-Mail in Rails finden Sie in diesem Artikel: https://medium.com/@coorasse/the-correct-emails-configuration-in-rails-c1d8418c0bfd

Coorasse
quelle
2
ActionMailer::MailDeliveryJobActionMailer::DeliveryJob
Musste
Das ist eine großartige Antwort!
Holger Frohloff vor
10

Füge das hinzu:

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

Referenz: http://mrlab.sk/testing-email-delivery-with-deliver-later.html

Minimul
quelle
4
Das hat bei mir funktioniert, aber ich habe es benutzt deliver_later(wait: 2.minutes). Also habe ichdeliver_later(options={})
rigelstpierre
8
Apps können synchronisierte und asynchrone E-Mails senden. Dies ist ein Hack, der es unmöglich macht, den Unterschied bei Tests zu erkennen.
Jeriko
2
Ich stimme zu, dass der Hack eine schlechte Idee ist. Ein späteres Aliasing endet nur mit Schmerzen.
John Paul Ashenfelter
2
Dieser Link ist tot, aber ich habe ihn auf dem Rückweg gefunden. web.archive.org/web/20150710184659/http://www.mrlab.sk/…
OzBarry
Ich bekommeNameError: uninitialized constant ActionMailer
Albert Català
10

Eine schönere Lösung (als Monkeypatching deliver_later) ist:

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

Die around { |example| perform_enqueued_jobs(&example) }sorgt dafür , dass Hintergrundaufgaben werden ausgeführt werden, bevor die Testwerte zu überprüfen.

Qqwy
quelle
Dieser Ansatz ist definitiv intuitiver zu verstehen, kann jedoch Ihre Tests erheblich verlangsamen, wenn die betreffende Aktion zeitaufwändige Jobs in die Warteschlange stellt.
Niborg
Dies testet auch nicht, welcher Mailer / welche Aktion ausgewählt wird. Wenn Ihr Code das bedingte Auswählen einer anderen Mail beinhaltet, hilft dies nicht
Cyril Duchon-Doris
4

Ich kam mit dem gleichen Zweifel und beschloss auf eine weniger ausführliche (einzeilige) Weise, inspiriert von dieser Antwort

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

Beachten Sie, dass der letzte with(no_args)wesentlich ist.

Aber wenn Sie sich nicht darum kümmern, ob deliver_laterangerufen wird, tun Sie einfach:

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

Luccas
quelle
4

Ein einfacher Weg ist:

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

Für aktuelle Googler:

allow(YourMailer).to receive(:mailer_method).and_call_original

expect(YourMailer).to have_received(:mailer_method)
Kronen4 Tage
quelle
2

Diese Antwort ist für Rails Test, nicht für rspec ...

Wenn Sie delivery_laterwie folgt verwenden:

# app/controllers/users_controller.rb 

class UsersController < ApplicationController def create # Yes, Ruby 2.0+ keyword arguments are preferred 
    UserMailer.welcome_email(user: @user).deliver_later 
  end 
end 

Sie können in Ihrem Test überprüfen, ob die E-Mail zur Warteschlange hinzugefügt wurde:

# test/controllers/users_controller_test.rb 

require 'test_helper' 

class UsersControllerTest < ActionController::TestCase 
  … 
  test 'email is enqueued to be delivered later' do 
    assert_enqueued_jobs 1 do 
      post :create, {…} 
    end 
  end 
end 

Wenn Sie dies jedoch tun, werden Sie von dem fehlgeschlagenen Test überrascht sein, der besagt, dass assert_enqueued_jobs für uns nicht definiert ist.

Dies liegt daran, dass unser Test von ActionController :: TestCase erbt, das zum Zeitpunkt des Schreibens ActiveJob :: TestHelper nicht enthält.

Aber wir können das schnell beheben:

# test/test_helper.rb 

class ActionController::TestCase 
  include ActiveJob::TestHelper 
  … 
end 

Referenz: https://www.engineyard.com/blog/testing-async-emails-rails-42

Maurymmarken
quelle
0

Ich bin hierher gekommen, um eine Antwort für einen vollständigen Test zu suchen. Ich habe also nicht nur gefragt, ob eine E-Mail darauf wartet, gesendet zu werden, sondern auch den Empfänger, den Betreff usw.

Ich habe eine Lösung, als von hier kommt , aber mit einer kleinen Änderung:

Wie es heißt, ist der kuriale Teil

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

Das Problem ist, dass sich die Parameter, die der Mailer empfängt, in diesem Fall von den Parametern unterscheiden, die in der Produktion empfangen werden. Wenn der erste Parameter ein Modell ist, erhält der jetzt im Test befindliche Parameter einen Hash und stürzt ab

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

Wenn wir also den Mailer so anrufen, wie UserMailer.welcome_email(@user).deliver_laterder Mailer in der Produktion einen Benutzer empfängt, wird er beim Testen empfangen{"_aj_globalid"=>"gid://forjartistica/User/1"}

Alle Kommentare werden geschätzt. Die weniger schmerzhafte Lösung, die ich gefunden habe, besteht darin, die Art und Weise zu ändern, wie ich die Mailer anrufe, die ID des Modells und nicht das Modell weiterzuleiten:

UserMailer.welcome_email(@user.id).deliver_later

Albert Català
quelle
0

Diese Antwort ist etwas anders, kann jedoch in Fällen wie einer neuen Änderung der Rails-API oder einer Änderung der Art und Weise, wie Sie liefern möchten, hilfreich sein (z. B. Verwendung deliver_nowanstelle von deliver_later).

Was ich die meiste Zeit mache, ist, einen Mailer als Abhängigkeit von der Methode zu übergeben, die ich teste, aber ich übergebe keinen Mailer von Schienen, sondern übergebe stattdessen ein Objekt, das die Dinge auf die "Art und Weise" erledigt Ich will"...

Wenn ich zum Beispiel überprüfen möchte, ob ich nach der Registrierung eines Benutzers die richtige Mail sende ... könnte ich ...

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

Und dann würde ich in dem Controller, in dem ich diese Methode aufrufen werde, die "echte" Implementierung dieses Mailers schreiben ...

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

Auf diese Weise habe ich das Gefühl, dass ich teste, dass ich die richtige Nachricht mit den richtigen Daten (Argumenten) an das richtige Objekt sende. Und ich muss nur ein sehr einfaches Objekt erstellen, das keine Logik hat, sondern nur die Verantwortung, zu wissen, wie ActionMailer aufgerufen werden soll.

Ich mache das lieber, weil ich lieber mehr Kontrolle über meine Abhängigkeiten habe. Dies ist für mich ein Beispiel für das "Prinzip der Abhängigkeitsinversion" .

Ich bin nicht sicher, ob es Ihr Geschmack ist, aber es ist ein anderer Weg, um das Problem zu lösen =).

Benito Serna
quelle