Railsでメール周りのrspecを書く際にハマった点

業務中にメール周りのテストを書くことがありました。その時に思いっきりrspecのreceiveの挙動にはまったので、そのことの備忘録です。
今回はまった点は、以下の二点です。

- rspecのreceiveの挙動
- ユーザーに対して送られるメールの本文へのmatch 

自分がもう少し柔軟に考えて色々試行錯誤していれば、そこまではまることもなかったんですが、今回はある意味思考停止状態に陥っていました。
先にテスト対象のクラスとそのクラスへの修正前のテストを書きます。
※必要な部分のみ抜き出してだいぶ簡素化しますので、実際のコードとは大きく違います。

## テスト対象のコード

UsersController#send_emailへpostされたら、emailの値からユーザーを持ってきて、そのユーザーに対してメールを送るという処理を行っています。
User#send_email内で実際のメール送信のための処理は、Sorceryのdeliver_reset_password_instructionsに移譲しています。
最終的には、ActionMailer#deliver_nowでメールを送信するという流れです。

```ruby:users_controller.rb
class UsersController < ApplicationController
  def send_email
    @user = User.find_by_email(params[:email])
    @user.send_email
    respond_with_json
  end
end
```

## 失敗時のテストコード

このテストでは、以下の点をテストしようとしています。

- User#send_emailが呼ばれていること
- メールが一通送られていること
- 送られたメールの本文に想定したURLが記載されていること

```ruby:users_controller_spec.rb
describe UsersController do
  describe 'POST#send_email' do
    specify 'email is sent' do
      expect(user).to receive(:send_email)

      post :send_email, email: user.email, language: language, format: 'json'

      expect(ActionMailer::Base.deliveries.size).to eq(1)
      expect(ActionMailer::Base.deliveries.first.body.encoded).to match(%r!http\S+?/send_email/\w{30}\b!)
    end
  end
end
```

ちなみに、`expect(user).to receive(:send_email)`は元々書かれていたテストで、以下の二行が私が足した行になります。

## はまった点

そもそもrspecでのテストを殆ど書いたことがなかったのですが、以前JUnitで書いたことはあり、書籍などでも少し触れていたので、なんとなくコードを読んだり書いたりすることはできました。
しかし、やはりなんとなく程度ではいけませんね。冒頭に書いたとおり、ガッツリはまってしまいました。

### rspecのreceiveの挙動

上述したとおり、`expect(user).to receive(:send_email)`は元々あったコードで、その下の二行が私が足したコードです。
で、その二行を足してrspecを実行したら、見事に失敗する。。。私が足した二行も以前似たようなテストを書いていたところから丸々パクってきていたので、なぜ失敗するのかわかりませんでした。

```ruby:result.rb
Failure/Error: expect(ActionMailer::Base.deliveries.size).to eq(1)

  expected: 1
       got: 0
```
ガッツリはまったあと先輩に聞きに行ったら、もしかしたらreceiveが悪さをしている可能性があるから、receiveの行をコメントアウトしてから試してみたら？という助言をもらい、試してみました。そしたら、見事に動くようになりました。
私の中で`receive`は、そのメソッドが呼ばれたことを確認するためのメソッドという認識だったのですが、どうやらmock的な働きもしていたようです。なので、実際に`ActionMailer#deliver_now`が呼ばれず、`ActionMailer::Base.deliveries.size`も0になっていたみたいです。
別の所で、`receive(:find_by_email).with(user.email).and_return user`みたいなコードを見ていたことで、意図的に`and_return`を書かなければ、実際のメソッドが呼ばれるだろうと、勝手に思っていたこともはまった原因の一つだと思います。
で、無事`expect(ActionMailer::Base.deliveries.size).to eq(1)`は成功するようになりましたが、次の行で失敗してしまいます。

### ユーザーに対して送られるメールの本文にmatchしない

メールの本文にこちらが想定した文字列が存在することを確認するためのテストが通りません。
`expect(ActionMailer::Base.deliveries.first.body.encoded).to match(%r!http\S+?/send_email/\w{30}\b!)`
失敗したときのエラーメッセージが以下のような感じ。
　```
+abcdefgbr><a href="http://www.example.com:3000/send_email/UPA=
+NVqfexCJfUWCWGDsxJzpjCzjfk" >http://www.example=abcdefgabcdefg=
```

こちらとしては、href属性の部分にmatchすることを望んでいるのですが、何故か右端に=が付与されてmatchしない。
これも先輩に助けてもらいました。いっしょにdebugしたりして情報を集めてなんとか以下のリンクにたどり着きました。
https://stackoverflow.com/questions/6487618/testing-actionmailer-multipart-emails-with-rspec/6588633#6588633

ここに書いてあるコードを参考にして、以下のように修正したら、動くようになりました。
`expect(ActionMailer::Base.deliveries.first.body.parts[1].body.raw_source).to match(%r!http\S+?/send_email/\w{30}\b!)`
正直何故このような記述にする必要があるのかの詳細はわかっていません。plain/textではなくhtml/textだからじゃない？とか色々話しましたが結論は出ませんでした。

### 最終的なテストコード
不要な部分を削ったり、必要な部分を足したりして、最終的には以下のコードでテストが成功するようになりました。

```ruby:users_controller_spec.rb
describe UsersController do
  describe 'POST#send_email' do
    specify 'email is sent' do
      post :send_email, email: user.email, format: 'json'

      expect(ActionMailer::Base.deliveries.size).to eq(1)
      expect(ActionMailer::Base.deliveries.first.body.parts[1].body.raw_source).to match(%r!http\S+?/send_email/\w{30}\b!)
    end
  end
end
```

## 反省
業務でRubyに触れたことがなく、JUnitでもメールのテストをしたことなどがなかったので、結構ガッツリはまってしまいました。
反省として以下の点をあげます。

  - コードを削るなり足すなりしてもう少し試行錯誤するべきだった
  - 似たようなテストがあるからと思って思考停止状態に陥っていた
  - もっと早い段階で先輩なり周りの人に聞くべきだった

それにしても先輩に聞いてからの解決までのスピードがやはり早い。自分のググり力を過信していました。
あと、できる人の問題解決の仕方を間近で見るのは本当に勉強になると思いました。特にRubyはinteractiveな環境が充実しているので、Rubyとどんどん会話して問題の原因と解決策を探っているところはとても勉強になりました。私もどんどんRubyと会話して仲良くなろうとお思います。

追記
家でちょっと探してみたらこんな記事もあったりしました。
[ActionMailer のメール送信テストを RSpec で行う](http://easyramble.com/test-action-mailer-with-rspec.html)