【テスト編】ActionMailerでメールを送信する
せっかくなのでTDDで。
方針
Emailのテストに関しては以下の記事が非常に丁寧にまとめてくださっていて感謝感激です。
ActionMailer のメール送信テストを RSpec で行う | EasyRamble
Emailのテストを書き始める前にRails Guidesのテストの項目に目を通しておくことを激オシしておきます。僕はちょっと進んでから先に読んでおいた方がよかったな、、、と若干後悔しましたので。
A Guide to Testing Rails Applications — Ruby on Rails Guides
Emailのテストの方針は以下です。
The goals of testing your mailer classes are to ensure that: ・emails are being processed (created and sent) ・the email content is correct (subject, sender, body, etc) ・the right emails are being sent at the right times
via: A Guide to Testing Rails Applications — Ruby on Rails Guides
これらの項目は、UnitテストとFunctionalテストで確認できるのですが、今回はcontrollerのfunctionalテストは書かずにfeatureテストを書く想定でやっていきたいと思いマッス!
There are two aspects of testing your mailer, the unit tests and the functional tests.
via: A Guide to Testing Rails Applications — Ruby on Rails Guides
下準備
まずはmailerとspecファイルを作成します。
$ rails g mailer UserMailer
everydayrailsで紹介されてる方法を参考にして、bmabey/email-specで提供されてるマッチャを使ってみようと思います。
testのグループにemail_specを追加してbundle installしておきます。
以下をrails_helper.rb
にそれぞれ追加します。(お使いのRSpecのバージョンに合わせてくだされ)
require"email_spec"
config.include(EmailSpec::Helpers) config.include(EmailSpec::Matchers)
READMEにあるように局所的にincludeしてもいいけどまぁすぐにDRYではなくなるよね。
describe "Signup Email" do include EmailSpec::Helpers include EmailSpec::Matchers # ... end
こんな感じでincludeしておけば、例えば以下のようなマッチャが使えるようになります。
expect(open_last_email).to be_delivered_from sender.email expect(open_last_email).to have_reply_to sender.email expect(open_last_email).to be_delivered_to recipient.email expect(open_last_email).to have_subject message.subject expect(open_last_email).to have_body_text message.message
RSpecのmatherは以下参考に。
https://github.com/bmabey/email-spec#rspec-matchers
そしてもうひとつ。テスト環境時には、config/environments/test.rb
で以下のようになっていれば、
# Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test
メールは実際には送信されずに、送信済みキューに格納されます。
ActionMailer::Base.deliveries
メソッドでキューの中身を取得できます。ActionMailerのメソッド定義部分。
# Provides a list of emails that have been delivered by Mail::TestMailer delegate :deliveries, :deliveries=, to: Mail::TestMailer
ちょっと余談になってしまうけど、テスト環境ではMail::TestMailer
に処理が委託されるみたい。ん、名前空間がActionMailerではない。へー、ActionMailerではmikel/mailというgemを内部で使ってたのか。なのでテスト用クラスもここにあると。
Rubyのgemとクラスの探検をしてると戻ってこれなくなりそうなので、話を戻そう。(ゆっくりこの旅を続けるって気持ちも大切にしたいけど...)
Macro
最初に、よりDRYにかっこよくメール送信をテストするためのマクロを準備しておきます。
# spec/support/mailer_macros.rb module MailerMacros def last_email ActionMailer::Base.deliveries.last end def reset_email ActionMailer::Base.deliveries = [] end end
ちょっとしたことですが、これだけで結構読みやすくなりますね :ok_woman:
余談ですが、RSpecのbefore/afterのフックをちゃんと理解できてませんでしたーーーここに一覧が載ってます。
`before` and `after` hooks - Hooks - RSpec Core - RSpec - Relish
:example
は:each
と一緒、:context
は:all
と一緒ということか。:context
と:all
一緒なの違和感?
Note: the :example and :context scopes are also available as :each and :all, respectively. Use whichever you prefer.
そして大切なのは実行手順ではなくて実はコレ。
:eachは変数の書き換えやトランザクション発行を全てそのスコープ内限定(今回だとit サインイン出来る)で利用出来るようにしています。 :eachで用意したデータは特定のitでのみ利用するいう意味合いを含むためこのような仕様になっているのかと思います。
あぶない、これはハマりそう...。
以下の記事でかなり詳細にまとめてくださってるので目を通して理解しておいた方がよいかなと思いました。
RSpecのhookのaliasについてはここで議論されてますね。
Adds hook scope aliases example
and context
#1174
まとめると、こんな感じになるかと思います。
RSpec.configure do |c| c.before(:each) { } # 全てのテストスイート中のそれぞれのexampleの前に実行される c.before(:example) { } # :eachのalias c.before(:all) { } # それぞれのトップレベルのグループの最初のexampleの前に実行される c.before(:context) { } # :allのalias c.before(:suite) { } # 全てのspecファイルがロードされたあと、最初のspecが実行される前に一度だけ実行される end # via: http://nilp.hatenablog.com/entry/2014/05/28/003335
こう見ると、サンプルのテキストは:all
でやってるみたいですが、:each
の方がいい気がしてきます。確かに:all
で事足りはするのですが、それだとEmailが他のexampleで残ってしまっていたりした場合どうなんだろう。問題はないけど、キューに残ってしまいそうな気はする。。?
以下のようにMailerMacrosをincludeすればれreset_emailが使えるようになりますね。
RSpec.configure do |config| config.include MailerMacros config.before :each do reset_email # ... end
Unitテスト
Unitテストでは、emailが作成され送信されることと、emailの中身(subject, sender, bodyとか)が正しいことの2つをテストします。
以下のような感じで書きましたー。
require "rails_helper" RSpec.describe UserMailer, type: :mailer do describe "when to register free membership" do let(:user) { create(:user) } let(:mail) { UserMailer.welcome_free_membership(user) } it "sends an email" do expect do mail.deliver_now end.to change { ActionMailer::Base.deliveries.size }.by(1) end it "renders the subject" do expect(mail.subject).to eq "無料会員登録が完了しました" end it "renders the receiver email" do expect(mail.to).to eq [user.email] end it "renders the sender email" do expect(mail.from).to eq ["info@1-box.co.jp"] end it "assigns @confirmation_url" do expect(mail.body.encoded).to match root_path end end end
注意点としては、mail.toとmail.fromは複数の場合もありえるので配列で答えが返ってくるということ。
Featureテスト
続いて機能のテスト。Featureテストでは以下の方針で書いていきます。
今回の想定では、ユーザが会員登録を行う流れで、適切にメールが送信されてるかをテストしたいので、ActionMailer::Base.deliveries.sizeが1増加することと、適切な相手に送られていること確認すればいいかなと思います。メールの内容はUnitテストで確認してるから不必要と判断。
以下のような感じに書いてみました。
describe "user registration page" do it "should create a new user" do # some expectations expect do click_link "登録する" end.to change { ActionMailer::Base.deliveries.size }.by(1) expect(last_email.to).to eq user.email # some expectations end end
ActionMailer::Base.deliveries.size
を調べるとこで80字超えちゃってちょっと不恰好になってしまい笑
ということでfeatureテストはこんな感じで。
さいごに
everydayrailsのRSpec本でrailscastsのテストの動画に触れられてた。今度みてみよう。
ということで次は実装編です〜〜〜