Skip to content

Commit 08e1460

Browse files
committed
Implement send_email matcher
1 parent 4a1d800 commit 08e1460

File tree

4 files changed

+359
-0
lines changed

4 files changed

+359
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Feature: `send_email` matcher
2+
3+
The `send_email` matcher is used to check if an email with the given parameters has been sent inside the expectation block.
4+
5+
NOTE: It implies that the spec example actually sends the email using the test adapter and does not schedule it for background.
6+
7+
To have an email sent in tests make sure:
8+
- `ActionMailer` performs deliveries - `Rails.application.config.action_mailer.perform_deliveries = true`
9+
- If the email is sent asynchronously (with `.deliver_later` call), ActiveJob uses the inline adapter - `Rails.application.config.active_job.queue_adapter = :inline`
10+
- ActionMailer uses the test adapter - `Rails.application.config.action_mailer.delivery_method = :test`
11+
12+
If you want to check an email has been scheduled for background, use the `have_enqueued_email` matcher.
13+
14+
Scenario: Checking email sent with the given multiple parameters
15+
Given a file named "spec/mailers/notifications_mailer_spec.rb" with:
16+
"""ruby
17+
require "rails_helper"
18+
19+
RSpec.describe NotificationsMailer do
20+
it "checks email sending by multiple params" do
21+
expect {
22+
NotificationsMailer.signup.deliver_now
23+
}.to send_email(
24+
25+
26+
subject: 'Signup'
27+
)
28+
end
29+
end
30+
"""
31+
When I run `rspec spec/mailers/notifications_mailer_spec.rb`
32+
Then the examples should all pass
33+
34+
Scenario: Checking email sent with matching parameters
35+
Given a file named "spec/mailers/notifications_mailer_spec.rb" with:
36+
"""ruby
37+
require "rails_helper"
38+
39+
RSpec.describe NotificationsMailer do
40+
it "checks email sending by one param only" do
41+
expect {
42+
NotificationsMailer.signup.deliver_now
43+
}.to send_email(
44+
45+
)
46+
end
47+
end
48+
"""
49+
When I run `rspec spec/mailers/notifications_mailer_spec.rb`
50+
Then the examples should all pass
51+
52+
Scenario: Checking email not sent with the given parameters
53+
Given a file named "spec/mailers/notifications_mailer_spec.rb" with:
54+
"""ruby
55+
require "rails_helper"
56+
57+
RSpec.describe NotificationsMailer do
58+
it "checks email not sent" do
59+
expect {
60+
NotificationsMailer.signup.deliver_now
61+
}.to_not send_email(
62+
63+
)
64+
end
65+
end
66+
"""
67+
When I run `rspec spec/mailers/notifications_mailer_spec.rb`
68+
Then the examples should all pass

lib/rspec/rails/matchers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module Matchers
2020
require 'rspec/rails/matchers/relation_match_array'
2121
require 'rspec/rails/matchers/be_valid'
2222
require 'rspec/rails/matchers/have_http_status'
23+
require 'rspec/rails/matchers/send_email'
2324

2425
if RSpec::Rails::FeatureCheck.has_active_job?
2526
require 'rspec/rails/matchers/active_job'
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# frozen_string_literal: true
2+
3+
module RSpec
4+
module Rails
5+
module Matchers
6+
# @api private
7+
#
8+
# Matcher class for `send_email`. Should not be instantiated directly.
9+
#
10+
# @see RSpec::Rails::Matchers#send_email
11+
class SendEmail < RSpec::Rails::Matchers::BaseMatcher
12+
# @api private
13+
# Define the email attributes that should be included in the inspection output.
14+
INSPECT_EMAIL_ATTRIBUTES = %i[subject from to cc bcc].freeze
15+
16+
def initialize(criteria)
17+
@criteria = criteria
18+
end
19+
20+
# @api private
21+
def supports_value_expectations?
22+
false
23+
end
24+
25+
# @api private
26+
def supports_block_expectations?
27+
true
28+
end
29+
30+
def matches?(block)
31+
define_matched_emails(block)
32+
33+
@matched_emails.one?
34+
end
35+
36+
# @api private
37+
# @return [String]
38+
def failure_message
39+
result =
40+
if multiple_match?
41+
"More than 1 matching emails were sent."
42+
else
43+
"No matching emails were sent."
44+
end
45+
"#{result}#{sent_emails_message}"
46+
end
47+
48+
# @api private
49+
# @return [String]
50+
def failure_message_when_negated
51+
"Expected not to send an email but it was sent."
52+
end
53+
54+
private
55+
56+
def diffable?
57+
true
58+
end
59+
60+
def deliveries
61+
ActionMailer::Base.deliveries
62+
end
63+
64+
def define_matched_emails(block)
65+
before = deliveries.dup
66+
67+
block.call
68+
69+
after = deliveries
70+
71+
@diff = after - before
72+
@matched_emails = @diff.select(&method(:matched_email?))
73+
end
74+
75+
def matched_email?(email)
76+
@criteria.all? do |attr, value|
77+
expected =
78+
case attr
79+
when :to, :from, :cc, :bcc then Array(value)
80+
else
81+
value
82+
end
83+
84+
values_match?(expected, email.public_send(attr))
85+
end
86+
end
87+
88+
def multiple_match?
89+
@matched_emails.many?
90+
end
91+
92+
def sent_emails_message
93+
if @diff.empty?
94+
"\n\nThere were no any emails sent inside the expectation block."
95+
else
96+
sent_emails =
97+
@diff.map do |email|
98+
inspected = INSPECT_EMAIL_ATTRIBUTES.map { |attr| "#{attr}: #{email.public_send(attr)}" }.join(", ")
99+
"- #{inspected}"
100+
end.join("\n")
101+
"\n\nThe following emails were sent:\n#{sent_emails}"
102+
end
103+
end
104+
end
105+
106+
# @api public
107+
# Check email sending with specific parameters.
108+
#
109+
# @example Positive expectation
110+
# expect { action }.to send_email
111+
#
112+
# @example Negative expectations
113+
# expect { action }.not_to send_email
114+
#
115+
# @example More precise expectation with attributes to match
116+
# expect { action }.to send_email(to: '[email protected]', subject: 'Confirm email')
117+
def send_email(criteria = {})
118+
SendEmail.new(criteria)
119+
end
120+
end
121+
end
122+
end
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
RSpec.describe "send_email" do
2+
let(:mailer) do
3+
Class.new(ActionMailer::Base) do
4+
self.delivery_method = :test
5+
6+
def test_email
7+
mail(
8+
9+
10+
11+
12+
subject: "Test email",
13+
body: "Test email body"
14+
)
15+
end
16+
end
17+
end
18+
19+
it "checks email sending by all params together" do
20+
expect {
21+
mailer.test_email.deliver_now
22+
}.to send_email(
23+
24+
25+
26+
27+
subject: "Test email",
28+
body: a_string_including("Test email body")
29+
)
30+
end
31+
32+
it "checks email sending by no params" do
33+
expect {
34+
mailer.test_email.deliver_now
35+
}.to send_email
36+
end
37+
38+
it "with to_not" do
39+
expect {
40+
mailer.test_email.deliver_now
41+
}.to_not send_email(
42+
43+
)
44+
end
45+
46+
it "fails with a clear message" do
47+
expect {
48+
expect { mailer.test_email.deliver_now }.to send_email(from: '[email protected]')
49+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
50+
No matching emails were sent.
51+
52+
The following emails were sent:
53+
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
54+
MSG
55+
end
56+
57+
it "fails with a clear message when no emails were sent" do
58+
expect {
59+
expect { }.to send_email
60+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
61+
No matching emails were sent.
62+
63+
There were no any emails sent inside the expectation block.
64+
MSG
65+
end
66+
67+
it "fails with a clear message for negated version" do
68+
expect {
69+
expect { mailer.test_email.deliver_now }.to_not send_email(from: "[email protected]")
70+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, "Expected not to send an email but it was sent.")
71+
end
72+
73+
it "fails for multiple matches" do
74+
expect {
75+
expect { 2.times { mailer.test_email.deliver_now } }.to send_email(from: "[email protected]")
76+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
77+
More than 1 matching emails were sent.
78+
79+
The following emails were sent:
80+
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
81+
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
82+
MSG
83+
end
84+
85+
context "with compound matching" do
86+
it "works when both matchings pass" do
87+
expect {
88+
expect {
89+
mailer.test_email.deliver_now
90+
}.to send_email(to: "[email protected]").and send_email(from: "[email protected]")
91+
}.to_not raise_error
92+
end
93+
94+
it "works when first matching fails" do
95+
expect {
96+
expect {
97+
mailer.test_email.deliver_now
98+
}.to send_email(to: "[email protected]").and send_email(to: "[email protected]")
99+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
100+
No matching emails were sent.
101+
102+
The following emails were sent:
103+
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
104+
MSG
105+
end
106+
107+
it "works when second matching fails" do
108+
expect {
109+
expect {
110+
mailer.test_email.deliver_now
111+
}.to send_email(to: "[email protected]").and send_email(to: "[email protected]")
112+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
113+
No matching emails were sent.
114+
115+
The following emails were sent:
116+
- subject: Test email, from: ["[email protected]"], to: ["[email protected]"], cc: ["[email protected]"], bcc: ["[email protected]"]
117+
MSG
118+
end
119+
end
120+
121+
context "with a custom negated version defined" do
122+
define_negated_matcher :not_send_email, :send_email
123+
124+
it "works with a negated version" do
125+
expect {
126+
mailer.test_email.deliver_now
127+
}.to not_send_email(
128+
129+
)
130+
end
131+
132+
it "fails with a clear message" do
133+
expect {
134+
expect { mailer.test_email.deliver_now }.to not_send_email(from: "[email protected]")
135+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, "Expected not to send an email but it was sent.")
136+
end
137+
138+
context "with a compound negated version" do
139+
it "works when both matchings pass" do
140+
expect {
141+
expect {
142+
mailer.test_email.deliver_now
143+
}.to not_send_email(to: "[email protected]").and not_send_email(from: "[email protected]")
144+
}.to_not raise_error
145+
end
146+
147+
it "works when first matching fails" do
148+
expect {
149+
expect {
150+
mailer.test_email.deliver_now
151+
}.to not_send_email(to: "[email protected]").and send_email(to: "[email protected]")
152+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, a_string_including(<<~MSG.strip))
153+
Expected not to send an email but it was sent.
154+
MSG
155+
end
156+
157+
it "works when second matching fails" do
158+
expect {
159+
expect {
160+
mailer.test_email.deliver_now
161+
}.to send_email(to: "[email protected]").and not_send_email(to: "[email protected]")
162+
}.to raise_error(RSpec::Expectations::ExpectationNotMetError, a_string_including(<<~MSG.strip))
163+
Expected not to send an email but it was sent.
164+
MSG
165+
end
166+
end
167+
end
168+
end

0 commit comments

Comments
 (0)