【テスト編】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を内部で使ってたのか。なのでテスト用クラスもここにあると。

mikel/mail · GitHub

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でのみ利用するいう意味合いを含むためこのような仕様になっているのかと思います。

via: http://ufun.hatenablog.com/entry/2014/08/28/004004

あぶない、これはハマりそう...。

以下の記事でかなり詳細にまとめてくださってるので目を通して理解しておいた方がよいかなと思いました。

RSpec 3の重要な変更 - 有頂天Ruby

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のテストの動画に触れられてた。今度みてみよう。

#275 How I Test - RailsCasts

ということで次は実装編です〜〜〜