Skip to content
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

Implement send_email matcher #2670

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 68 additions & 0 deletions features/matchers/send_email_matcher.feature
@@ -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.
JonRowe marked this conversation as resolved.
Show resolved Hide resolved

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.
JonRowe marked this conversation as resolved.
Show resolved Hide resolved

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: 'from@example.com',
to: 'to@example.org',
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: 'to@example.org'
)
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: 'no@example.org'
)
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
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
@@ -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?
ka8725 marked this conversation as resolved.
Show resolved Hide resolved
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."
pirj marked this conversation as resolved.
Show resolved Hide resolved
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: 'test@example.com', 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
@@ -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: "from@example.com",
cc: "cc@example.com",
bcc: "bcc@example.com",
to: "to@example.com",
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: "from@example.com",
to: "to@example.com",
cc: "cc@example.com",
bcc: "bcc@example.com",
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: "failed@example.com"
)
end

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

The following emails were sent:
- subject: Test email, from: ["from@example.com"], to: ["to@example.com"], cc: ["cc@example.com"], bcc: ["bcc@example.com"]
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: "from@example.com")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, "Expected not to send an email but it was sent.")
end
pirj marked this conversation as resolved.
Show resolved Hide resolved

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

The following emails were sent:
- subject: Test email, from: ["from@example.com"], to: ["to@example.com"], cc: ["cc@example.com"], bcc: ["bcc@example.com"]
- subject: Test email, from: ["from@example.com"], to: ["to@example.com"], cc: ["cc@example.com"], bcc: ["bcc@example.com"]
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: "to@example.com").and send_email(from: "from@example.com")
}.to_not raise_error
end

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

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

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

The following emails were sent:
- subject: Test email, from: ["from@example.com"], to: ["to@example.com"], cc: ["cc@example.com"], bcc: ["bcc@example.com"]
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: "failed@example.com"
)
end

it "fails with a clear message" do
expect {
expect { mailer.test_email.deliver_now }.to not_send_email(from: "from@example.com")
}.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: "noto@example.com").and not_send_email(from: "nofrom@example.com")
}.to_not raise_error
end

it "works when first matching fails" do
expect {
expect {
mailer.test_email.deliver_now
}.to not_send_email(to: "to@example.com").and send_email(to: "to@example.com")
}.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: "to@example.com").and not_send_email(to: "to@example.com")
}.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