Skip to content

Implement send_email matcher #2670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions features/matchers/send_email_matcher.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
Feature: `send_email` matcher

The `send_email` matcher is used to check if an email with the given parameters has been sent inside the expectation block.

NOTE: It implies that the spec example actually sends the email using the test adapter and does not schedule it for background.

To have an email sent in tests make sure:
- `ActionMailer` performs deliveries - `Rails.application.config.action_mailer.perform_deliveries = true`
- If the email is sent asynchronously (with `.deliver_later` call), ActiveJob uses the inline adapter - `Rails.application.config.active_job.queue_adapter = :inline`
- ActionMailer uses the test adapter - `Rails.application.config.action_mailer.delivery_method = :test`

If you want to check an email has been scheduled for background, use the `have_enqueued_email` matcher.

Scenario: Checking email sent with the given multiple parameters
Given a file named "spec/mailers/notifications_mailer_spec.rb" with:
"""ruby
require "rails_helper"

RSpec.describe NotificationsMailer do
it "checks email sending by multiple params" do
expect {
NotificationsMailer.signup.deliver_now
}.to send_email(
from: '[email protected]',
to: '[email protected]',
subject: 'Signup'
)
end
end
"""
When I run `rspec spec/mailers/notifications_mailer_spec.rb`
Then the examples should all pass

Scenario: Checking email sent with matching parameters
Given a file named "spec/mailers/notifications_mailer_spec.rb" with:
"""ruby
require "rails_helper"

RSpec.describe NotificationsMailer do
it "checks email sending by one param only" do
expect {
NotificationsMailer.signup.deliver_now
}.to send_email(
to: '[email protected]'
)
end
end
"""
When I run `rspec spec/mailers/notifications_mailer_spec.rb`
Then the examples should all pass

Scenario: Checking email not sent with the given parameters
Given a file named "spec/mailers/notifications_mailer_spec.rb" with:
"""ruby
require "rails_helper"

RSpec.describe NotificationsMailer do
it "checks email not sent" do
expect {
NotificationsMailer.signup.deliver_now
}.to_not send_email(
to: '[email protected]'
)
end
end
"""
When I run `rspec spec/mailers/notifications_mailer_spec.rb`
Then the examples should all pass
1 change: 1 addition & 0 deletions lib/rspec/rails/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Matchers
require 'rspec/rails/matchers/relation_match_array'
require 'rspec/rails/matchers/be_valid'
require 'rspec/rails/matchers/have_http_status'
require 'rspec/rails/matchers/send_email'

if RSpec::Rails::FeatureCheck.has_active_job?
require 'rspec/rails/matchers/active_job'
Expand Down
122 changes: 122 additions & 0 deletions lib/rspec/rails/matchers/send_email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# frozen_string_literal: true

module RSpec
module Rails
module Matchers
# @api private
#
# Matcher class for `send_email`. Should not be instantiated directly.
#
# @see RSpec::Rails::Matchers#send_email
class SendEmail < RSpec::Rails::Matchers::BaseMatcher
# @api private
# Define the email attributes that should be included in the inspection output.
INSPECT_EMAIL_ATTRIBUTES = %i[subject from to cc bcc].freeze

def initialize(criteria)
@criteria = criteria
end

# @api private
def supports_value_expectations?
false
end

# @api private
def supports_block_expectations?
true
end

def matches?(block)
define_matched_emails(block)

@matched_emails.one?
end

# @api private
# @return [String]
def failure_message
result =
if multiple_match?
"More than 1 matching emails were sent."
else
"No matching emails were sent."
end
"#{result}#{sent_emails_message}"
end

# @api private
# @return [String]
def failure_message_when_negated
"Expected not to send an email but it was sent."
end

private

def diffable?
true
end

def deliveries
ActionMailer::Base.deliveries
end

def define_matched_emails(block)
before = deliveries.dup

block.call

after = deliveries

@diff = after - before
@matched_emails = @diff.select(&method(:matched_email?))
end

def matched_email?(email)
@criteria.all? do |attr, value|
expected =
case attr
when :to, :from, :cc, :bcc then Array(value)
else
value
end

values_match?(expected, email.public_send(attr))
end
end

def multiple_match?
@matched_emails.many?
end

def sent_emails_message
if @diff.empty?
"\n\nThere were no any emails sent inside the expectation block."
else
sent_emails =
@diff.map do |email|
inspected = INSPECT_EMAIL_ATTRIBUTES.map { |attr| "#{attr}: #{email.public_send(attr)}" }.join(", ")
"- #{inspected}"
end.join("\n")
"\n\nThe following emails were sent:\n#{sent_emails}"
end
end
end

# @api public
# Check email sending with specific parameters.
#
# @example Positive expectation
# expect { action }.to send_email
#
# @example Negative expectations
# expect { action }.not_to send_email
#
# @example More precise expectation with attributes to match
# expect { action }.to send_email(to: '[email protected]', subject: 'Confirm email')
def send_email(criteria = {})
SendEmail.new(criteria)
end
end
end
end
168 changes: 168 additions & 0 deletions spec/rspec/rails/matchers/send_email_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
RSpec.describe "send_email" do
let(:mailer) do
Class.new(ActionMailer::Base) do
self.delivery_method = :test

def test_email
mail(
from: "[email protected]",
cc: "[email protected]",
bcc: "[email protected]",
to: "[email protected]",
subject: "Test email",
body: "Test email body"
)
end
end
end

it "checks email sending by all params together" do
expect {
mailer.test_email.deliver_now
}.to send_email(
from: "[email protected]",
to: "[email protected]",
cc: "[email protected]",
bcc: "[email protected]",
subject: "Test email",
body: a_string_including("Test email body")
)
end

it "checks email sending by no params" do
expect {
mailer.test_email.deliver_now
}.to send_email
end

it "with to_not" do
expect {
mailer.test_email.deliver_now
}.to_not send_email(
from: "[email protected]"
)
end

it "fails with a clear message" do
expect {
expect { mailer.test_email.deliver_now }.to send_email(from: '[email protected]')
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
No matching emails were sent.

The following emails were sent:
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
MSG
end

it "fails with a clear message when no emails were sent" do
expect {
expect { }.to send_email
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
No matching emails were sent.

There were no any emails sent inside the expectation block.
MSG
end

it "fails with a clear message for negated version" do
expect {
expect { mailer.test_email.deliver_now }.to_not send_email(from: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, "Expected not to send an email but it was sent.")
end

it "fails for multiple matches" do
expect {
expect { 2.times { mailer.test_email.deliver_now } }.to send_email(from: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
More than 1 matching emails were sent.

The following emails were sent:
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
MSG
end

context "with compound matching" do
it "works when both matchings pass" do
expect {
expect {
mailer.test_email.deliver_now
}.to send_email(to: "[email protected]").and send_email(from: "[email protected]")
}.to_not raise_error
end

it "works when first matching fails" do
expect {
expect {
mailer.test_email.deliver_now
}.to send_email(to: "[email protected]").and send_email(to: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
No matching emails were sent.

The following emails were sent:
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
MSG
end

it "works when second matching fails" do
expect {
expect {
mailer.test_email.deliver_now
}.to send_email(to: "[email protected]").and send_email(to: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
No matching emails were sent.

The following emails were sent:
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
MSG
end
end

context "with a custom negated version defined" do
define_negated_matcher :not_send_email, :send_email

it "works with a negated version" do
expect {
mailer.test_email.deliver_now
}.to not_send_email(
from: "[email protected]"
)
end

it "fails with a clear message" do
expect {
expect { mailer.test_email.deliver_now }.to not_send_email(from: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, "Expected not to send an email but it was sent.")
end

context "with a compound negated version" do
it "works when both matchings pass" do
expect {
expect {
mailer.test_email.deliver_now
}.to not_send_email(to: "[email protected]").and not_send_email(from: "[email protected]")
}.to_not raise_error
end

it "works when first matching fails" do
expect {
expect {
mailer.test_email.deliver_now
}.to not_send_email(to: "[email protected]").and send_email(to: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, a_string_including(<<~MSG.strip))
Expected not to send an email but it was sent.
MSG
end

it "works when second matching fails" do
expect {
expect {
mailer.test_email.deliver_now
}.to send_email(to: "[email protected]").and not_send_email(to: "[email protected]")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, a_string_including(<<~MSG.strip))
Expected not to send an email but it was sent.
MSG
end
end
end
end