Skip to content

Commit

Permalink
Implement send_email matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
ka8725 committed May 1, 2023
1 parent 4a1d800 commit b09c2bb
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 0 deletions.
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.

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: '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?
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: 'test@example.com', subject: 'Confirm email')
def send_email(criteria = {})
SendEmail.new(criteria)
end
end
end
end
173 changes: 173 additions & 0 deletions spec/rspec/rails/matchers/send_email_spec.rb
@@ -0,0 +1,173 @@
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

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
around do |example|
RSpec::Matchers.define_negated_matcher :not_send_email, :send_email
example.run
ensure
undef not_send_email
end

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

0 comments on commit b09c2bb

Please sign in to comment.