メーラーのRSpecテスト

メーラーRSpecテスト

メーラーRSpecテストでハマったことがあったので、備忘録としてこちらにまとめます。

コードは、以下のようになります。

[app/mailers/user_mailer.rb]

class UserMailer < ApplicationMailer

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.reset_password_email.subject
  #
  def reset_password_email(user)
    @user = User.find user.id
    @url  = edit_password_reset_url(@user.reset_password_token)
    mail(:to => user.email,
         :subject => "パスワード再発行のお知らせ")
  end
end
[app/views/user_mailer/reset_password_email.html.slim]

doctype html
html
  head
    meta[content="text/html; charset=UTF-8" http-equiv="Content-Type"]
  body
    p = "#{@user.name} 様"
    
    p パスワード再発行の依頼を受け付けました。
    p こちらのリンクからパスワードの再発行を行ってください。
    p = link_to nil, @url
[app/views/user_mailer/reset_password_email.text.slim]

= "#{@user.name} 様"

パスワード再発行の依頼を受け付けました。
こちらのリンクからパスワードの再発行を行ってください。
= @url
[app/mailers/user_mailer_spec.rb]

require "rails_helper"

RSpec.describe UserMailer, type: :mailer do
  describe 'パスワードリセットのメール送信の検証' do
    let(:user) { create(:user) }
    let(:mail) { UserMailer.reset_password_email(user) }
    before do
      user.generate_reset_password_token!
      mail.deliver_now
    end
    
    context 'メールを送信した時' do
      it 'ヘッダー情報,ボディ情報が正しい' do
        expect(mail.subject).to eq 'パスワード再発行のお知らせ'
        expect(mail.to).to eq [user.email]
        expect(mail.from).to eq ['from@example.com']
      end

      it 'メール本文が正しい' do
        expect(mail.html_part.body.to_s).to have_content "#{user.name} 様"
        expect(mail.html_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
        expect(mail.html_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
        expect(mail.html_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
        expect(mail.text_part.body.to_s).to have_content "#{user.name} 様"
        expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
        expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
        expect(mail.text_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
      end
    end
  end
end

上記のテストを実行すると以下のようなエラーになります。

higmonta@higuchiyuunoMBP fishing_cooking % bundle exec rspec spec/mailers/user_mailer_spec.rb
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <top (required)> at /Users/higmonta/workspace/fishing_cooking/config/environment.rb:5)

UserMailer
  パスワードリセットのメール送信の検証
    メールを送信した時
      ヘッダー情報,ボディ情報が正しい
      メール本文が正しい (FAILED - 1)

Failures:

  1) UserMailer パスワードリセットのメール送信の検証 メールを送信した時 メール本文が正しい
     Failure/Error: expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
       expected to find text "パスワード再発行の依頼を受け付けました。" in "大野 莉子 様。パスワード再発行の依頼を受け付けました>。こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/hf6PPCGouyRVPfAzi3i3/edit"
     # ./spec/mailers/user_mailer_spec.rb:25:in `block (4 levels) in <top (required)>'

Finished in 0.36584 seconds (files took 3.99 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/mailers/user_mailer_spec.rb:19 # UserMailer パスワードリセットのメール送信の検証 メールを送信した時 メール本文が正しい

デバッグして確認してみます。

higmonta@higuchiyuunoMBP fishing_cooking % bundle exec rspec spec/mailers/user_mailer_spec.rb
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <top (required)> at /Users/higmonta/workspace/fishing_cooking/config/environment.rb:5)

UserMailer
  パスワードリセットのメール送信の検証
    メールを送信した時
      ヘッダー情報,ボディ情報が正しい

^A^BFrom:^A^B /Users/higmonta/workspace/fishing_cooking/spec/mailers/user_mailer_spec.rb:24 :

    19:       it 'メール本文が正しい' do
    20:         expect(mail.html_part.body.to_s).to have_content "#{user.name} 様"
    21:         expect(mail.html_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
    22:         expect(mail.html_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
    23:         expect(mail.html_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
 => 24:         binding.pry
    25:         expect(mail.text_part.body.to_s).to have_content "#{user.name} 様"
    26:         expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
    27:         expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
    28:         expect(mail.text_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
    29:       end

[1] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.html_part.body.to_s
=> "<!DOCTYPE html><html><head><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><style> /* Email styles need to be inline */ </style></head><body><!DOCTYPE html><html><head><meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" /></head><body><p>村上 美穂 様</p><p>パスワード再発行の依頼を受け付けました。</p><p>こちらのリンクからパスワードの再発行を行ってください。</p><p><a href=\"http://localhost:3000/password_resets/BtLzvHUukixrNqAQoJJx/edit\">http://localhost:3000/password_resets/BtLzvHUukixrNqAQoJJx/edit</a></p></body></html></body></html>"
[2] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.text_part.body.to_s
=> "村上 美穂 様<パスワード再発行の依頼を受け付けました>。</パスワード再発行の依頼を受け付けました><こちらのリンクからパスワードの再発行を行ってください>。</こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/BtLzvHUukixrNqAQoJJx/edit"
[3] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content "#{user.name} 様"
=> true
[4] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました [4] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
RSpec::Expectations::ExpectationNotMetError: expected to find text "パスワード再発行の依頼を受け付けました。" in "村上 美穂 様。パスワード再発行の依頼を受け付けました>。こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/BtLzvHUukixrNqAQoJJx/edit"
from /Users/higmonta/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-support-3.11.0/lib/rspec/support.rb:102:in `block in <module:Support>'
[5] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を [5] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
RSpec::Expectations::ExpectationNotMetError: expected to find text "こちらのリンクからパスワードの再発行を行ってください。" in "村上 美穂 様。パスワード再発行の依頼を受け付けました>。こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/BtLzvHUukixrNqAQoJJx/edit"
from /Users/higmonta/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-support-3.11.0/lib/rspec/support.rb:102:in `block in <module:Support>'
[6] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"n}/edit"
=> true 

下記2つだけがエラーになっています。(ユーザー名の文章部分とURLの部分はエラーになっていません。)
expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
下記のように期待している文章が入っているのでエラーにならないはずだが。。。。

[2] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.text_part.body.to_s
=> "村上 美穂 様<パスワード再発行の依頼を受け付けました>。</パスワード再発行の依頼を受け付けました><こちらのリンクからパスワードの再発行を行ってください>。</こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/BtLzvHUukixrNqAQoJJx/edit"

上記を確認するとエラーになっている文章の部分だけ<</>が入っており、これに問題がありそうと考えました。
そもそも、なぜここの文章だけが<</>が入っているのか再度メールのビューを確認しました。

[app/views/user_mailer/reset_password_email.text.slim]

= "#{@user.name} 様"

パスワード再発行の依頼を受け付けました。  ①
こちらのリンクからパスワードの再発行を行ってください。  ②
= @url

そもそもこれってslim形式→text形式に変換されるファイルなはずなのだが、①、②にhtmlタグを記載せずに文章だけ書いてしまっている。(textのように記載してしまっている。)
下記のように編集したところ、テストが通りました。

p = "#{@user.name} 様"

p パスワード再発行の依頼を受け付けました。
p こちらのリンクからパスワードの再発行を行ってください。
p = @url

デバッグで確認したら以下のようになり、<</>が無くなっていました。

higmonta@higuchiyuunoMBP fishing_cooking % bundle exec rspec spec/mailers/user_mailer_spec.rb
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <top (required)> at /Users/higmonta/workspace/fishing_cooking/config/environment.rb:5)

UserMailer
  パスワードリセットのメール送信の検証
    メールを送信した時
      ヘッダー情報,ボディ情報が正しい

^A^BFrom:^A^B /Users/higmonta/workspace/fishing_cooking/spec/mailers/user_mailer_spec.rb:24 :

    19:       it 'メール本文が正しい' do
    20:         expect(mail.html_part.body.to_s).to have_content "#{user.name} 様"
    21:         expect(mail.html_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
    22:         expect(mail.html_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
    23:         expect(mail.html_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
 => 24:         binding.pry
    25:         expect(mail.text_part.body.to_s).to have_content "#{user.name} 様"
    26:         expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
    27:         expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
    28:         expect(mail.text_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
    29:       end

[1] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.text_part.body.to_s
=> "<p>酒井 仁 様</p><p>パスワード再発行の依頼を受け付けました。</p><p>こちらのリンクからパスワードの再発行を行ってください。</p><p>http://localhost:3000/password_resets/mwxXk4h9vCMZfHwvXnRz/edit</p>"
[2] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content "#{user.name} 様"ame} 様"
=> true 
[3] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました [3] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
=> true
[4] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を [4] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
=> true
[5] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> expect(mail.text_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
=> true

※ポイント
mail.html_part.body.to_s:マルチパートメールにしている場合にhtml形式のメール本文の検証ができる。
mail.text_part.body.to_s:マルチパートメールにしている場合にtext形式のメール本文の検証ができる。

それぞれのメソッドがどのような動きになっているのか気になったので、デバッグして確認した結果が以下になります。

higmonta@higuchiyuunoMBP fishing_cooking % bundle exec rspec spec/mailers/user_mailer_spec.rb
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <top (required)> at /Users/higmonta/workspace/fishing_cooking/config/environment.rb:5)

UserMailer
  パスワードリセットのメール送信の検証
    メールを送信した時
      ヘッダー情報,ボディ情報が正しい

^A^BFrom:^A^B /Users/higmonta/workspace/fishing_cooking/spec/mailers/user_mailer_spec.rb:24 :

    19:       it 'メール本文が正しい' do
    20:         expect(mail.html_part.body.to_s).to have_content "#{user.name} 様"
    21:         expect(mail.html_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
    22:         expect(mail.html_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
    23:         expect(mail.html_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
 => 24:         binding.pry
    25:         expect(mail.text_part.body.to_s).to have_content "#{user.name} 様"
    26:         expect(mail.text_part.body.to_s).to have_content 'パスワード再発行の依頼を受け付けました。'
    27:         expect(mail.text_part.body.to_s).to have_content 'こちらのリンクからパスワードの再発行を行ってください。'
    28:         expect(mail.text_part.body.to_s).to have_content "http://localhost:3000/password_resets/#{user.reset_password_token}/edit"
    29:       end

[1] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail
=> #<Mail::Message:70296108227480, Multipart: true, Headers: <Date: Sun, 11 Sep 2022 04:46:08 +0900>, <From: from@example.com>, <To: delmer_brekke@hudson-hilll.co>, <Message-ID: <631ce980c1322_1665e3fef12436e6c5881e@higuchiyuunoMBP.mail>>, <Subject: パスワード再発行のお知らせ>, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; boundary="--==_mimepart_631ce980c0d0a_1665e3fef12436e6c5879e"; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>>
[2] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.html_part
=> #<Mail::Part:70296108194240, Multipart: false, Headers: <Content-Type: text/html>, <Content-Transfer-Encoding: base64>>
[3] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.html_part.body
=> #<Mail::Body:0x00007fde2d1d1f88
 @ascii_only=false,
 @boundary=nil,
 @charset=nil,
 @encoding="8bit",
 @epilogue=nil,
 @part_sort_order=["text/plain", "text/enriched", "text/html"],
 @parts=[],
 @preamble=nil,
 @raw_source=
  "<!DOCTYPE html><html><head><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><style> /* Email styles need to be inline */ </style></head><body><!DOCTYPE html><html><head><meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" /></head><body><p>千葉 一輝 様</p><p>パスワード再発行の依頼を受け付けました。</p><p>こちらのリンクからパスワードの再発行を行ってください。</p><p><a href=\"http://localhost:3000/password_resets/xyD6ASVsM1w6G-gXedCS/edit\">http://localhost:3000/password_resets/xyD6ASVsM1w6G-gXedCS/edit</a></p></body></html></body></html>">
[4] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.html_part.body.to_s
=> "<!DOCTYPE html><html><head><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><style> /* Email styles need to be inline */ </style></head><body><!DOCTYPE html><html><head><meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" /></head><body><p>千葉 一輝 様</p><p>パスワード再発行の依頼を受け付けました。</p><p>こちらのリンクからパスワードの再発行を行ってください。</p><p><a href=\"http://localhost:3000/password_resets/xyD6ASVsM1w6G-gXedCS/edit\">http://localhost:3000/password_resets/xyD6ASVsM1w6G-gXedCS/edit</a></p></body></html></body></html>"
[5] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.text_part
=> #<Mail::Part:70296108178900, Multipart: false, Headers: <Content-Type: text/plain>, <Content-Transfer-Encoding: base64>>
[6] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.text_part.body
=> #<Mail::Body:0x00007fde2d1d0f70
 @ascii_only=false,
 @boundary=nil,
 @charset=nil,
 @encoding="8bit",
 @epilogue=nil,
 @part_sort_order=["text/plain", "text/enriched", "text/html"],
 @parts=[],
 @preamble=nil,
 @raw_source=
  "千葉 一輝 様<パスワード再発行の依頼を受け付けました>。</パスワード再発行の依頼を受け付けました><こちらのリンクからパスワードの再発行を行ってください>。</こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/xyD6ASVsM1w6G-gXedCS/edit">
[7] pry(#<RSpec::ExampleGroups::UserMailer::Nested::Nested>)> mail.text_part.body.to_s
=> "千葉 一輝 様<パスワード再発行の依頼を受け付けました>。</パスワード再発行の依頼を受け付けました><こちらのリンクからパスワードの再発行を行ってください>。</こちらのリンクからパスワードの再発行を行ってください>http://localhost:3000/password_resets/xyD6ASVsM1w6G-gXedCS/edit"

参考記事

RSpecでメーラーのテスト実行時にメールの本文がなぜか空になっている問題を解決する - Qiita

rspec2 - rspec-email - How to get the body text? - Stack Overflow

Rails / Ruby : Mail の HTML テキスト を取得する方法 - Qiita

【rails】【rspec】RspecでRails のActionMailer の本文をテストする - Qiita