Devise confirmable用のテスト（フィーチャスペック）を書く（解説動画付き）


## はじめに

先日、Qiitaに「devise でメールアドレスのみでユーザー登録を行い、パスワードを後から設定する方法」という記事が投稿されていました。

[devise でメールアドレスのみでユーザー登録を行い、パスワードを後から設定する方法 - Qiita](http://qiita.com/necojackarc/items/bd3286064ed3cd40db6b)

上記の記事で紹介されているDevise confirmableは僕もよく使っています。

ただ、ユーザー登録のフローは手作業でテストしようとすると、毎回新しいユーザーを登録しなければならず手間がかかります。
このように気軽に動作確認できない操作は自動化テストで動作確認できるようにしておくことが望ましいです。

そこでこの記事ではDevise confirmable用のテストをRSpecで書く方法を紹介します。

## 使用するサンプルアプリケーション

@necojackarc さんが作成されたサンプルアプリを使います。

https://github.com/necojackarc/email-only-signup-with-devise

## 今回作成したテストコード

今回作成したテストコードは僕のリポジトリに置いてあります。

https://github.com/JunichiIto/email-only-signup-with-devise/tree/test-with-rspec

## Special bonus! 解説動画をアップしました

実際にテストを書いていく様子をYouTubeにアップしました。
本記事とあわせてこちらの動画も見てもらうと、何をどうやっているのかがより理解しやすくなるはずです。

[![Screen Shot 2016-06-15 at 09.36.14.png](https://qiita-image-store.s3.amazonaws.com/0/7465/63e315b6-31aa-1593-5223-2834f44b3990.png "Screen Shot 2016-06-15 at 09.36.14.png")Devise confirmable用のテスト（フィーチャスペック）を書く - YouTube](https://www.youtube.com/watch?v=4NWKO-eG6w4&feature=youtu.be)

また、IDEとして[RubyMine](https://www.jetbrains.com/ruby/)を使っているので、RubyMineを使ってみたい方、もしくはRubyMineをもっと使いこなしたいと思っている方にも参考になると思います。

### 動画の中ではショートカットキーが表示されます！

今回はどんなショートカットを使っているのかわかりやすくするために、画面にショートカットキーを表示しています。
もしかするとあなたの知らないRubyMineのショートカットキーが登場するかもしれませんよ？

![Screen Shot 2016-06-15 at 09.50.10.png](https://qiita-image-store.s3.amazonaws.com/0/7465/cb98e6fc-fa32-a6dd-2f24-411792072153.png "Screen Shot 2016-06-15 at 09.50.10.png")


それでは以下が手順です。

## 1. RSpec + Capybaraのセットアップ

元のサンプルアプリにはRSpecがセットアップされていないので、まずRSpecのセットアップをします。
また、フィーチャスペックを書くのでCapybaraも使います。
RSpecやCapybaraのセットアップ手順はネット等でよく紹介されているはずなので、ここでは省略します。
（前述の動画の中ではセットアップの手順も紹介しているので、動画を見てもらうのも一つの手です）

## 2. test.rb の変更

テスト環境で使用するホスト名を設定します。
ホスト名を指定しないとメール送信時にエラーが起きます。

```ruby:config/environments/test.rb
Rails.application.configure do
  # ...
  
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
end
```

## 3. database_rewinder のセットアップ

Devise confirmable は after_commit のコールバックでメールを送信します。
しかし、RSpecのデフォルトのトランザクション設定は `use_transactional_fixtures = true` になっており、テストの最後にロールバックするので、テスト実行中はコミットされません。
よって、そのままだとパスワード設定用のメールが送信されない、という問題があります。

そこでまず、`rails_helper.rb` でトランザクションを使わないように設定を変更します。

```ruby:spec/rails_helper.rb
RSpec.configure do |config|
  # ...
  
  config.use_transactional_fixtures = false
  
  # ...
end
```

これでテスト実行中にもコミットされるようになり、メール送信もできるようになります。

しかし、今度はテスト実行中に作成したデータがデータベース内に残ったままになる、という問題が起きます。
そこで次に、[database_rewinder](https://github.com/amatsuda/database_rewinder) というgemを使って、テスト終了時にデータを削除するようにします。

まずはgemをインストールします。

```Gemfile
group :test do
  # ...
  gem 'database_rewinder'
end
```

`bundle install` したら、`rails_helper.rb` に以下の設定を追加します。


```ruby:spec/rails_helper.rb
RSpec.configure do |config|
  # ...
  
  config.before(:suite) do
    DatabaseRewinder.clean_all
  end

  config.after(:each) do
    DatabaseRewinder.clean
  end
    
  # ...
end
```

なお、database_rewinder の代わりに [database_cleaner](https://github.com/DatabaseCleaner/database_cleaner) を使うのもOKです。

## 4. 元のアプリの不具合を修正する


元のサンプルアプリにはパスワードが未入力でもパスワード設定ができてしまう、という不具合があるのでそれを修正します。

これは不具合ではなく、意図的にバリデーションを外していたそうです。（[参考](http://qiita.com/necojackarc/items/bd3286064ed3cd40db6b#%E6%B3%A8%E6%84%8F)）
が、ここでは最低限のバリデーションを追加することにします。
（バリデーションの追加は[こちらのWikiページ](https://github.com/plataformatec/devise/wiki/How-To:-Email-only-sign-up)を参考にしています。）

```diff
 # app/models/user.rb
 
 class User < ActiveRecord::Base
   # Include default devise modules. Others available are:
   # :lockable, :timeoutable and :omniauthable
   devise :confirmable, :database_authenticatable, :registerable,
          :recoverable, :rememberable, :trackable, :validatable
 
   def password_required?
     super if confirmed?
   end
 
+  def password_match?
+    self.errors[:password] << "can't be blank" if password.blank?
+    self.errors[:password_confirmation] << "can't be blank" if password_confirmation.blank?
+    self.errors[:password_confirmation] << "does not match password" if password != password_confirmation
+    password == password_confirmation && !password.blank?
+  end
 end
```

```diff
 # app/controllers/users/confirmations_controller.rb
 
 class Users::ConfirmationsController < Devise::ConfirmationsController
   # ...
 
   def confirm
     confirmation_token = params[resource_name][:confirmation_token]
     self.resource = resource_class.find_by_confirmation_token!(confirmation_token)
 
-    if resource.update(confirmation_params)
+    if resource.update(confirmation_params) && resource.password_match?
       self.resource = resource_class.confirm_by_token(confirmation_token)
       set_flash_message :notice, :confirmed
       sign_in_and_redirect resource_name, resource
     else
       render :show
     end
   end
 
   # ...

   def confirmation_params
-    params.require(resource_name).permit(:password)
+    params.require(resource_name).permit(:password, :password_confirmation)
   end
 end
```

```diff
 # app/views/users/confirmations/show.html.erb
  <h2>Enter new password</h2>
 
 <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name)) do |f| %>
   <%= f.hidden_field :confirmation_token %>
 
   <%= devise_error_messages! %>
 
   <div class="field">
     <%= f.label :password %>
     <% if @minimum_password_length %>
     <em>(<%= @minimum_password_length %> characters minimum)</em>
     <% end %><br />
-    <%= f.password_field :password, autocomplete: "off" %>
+    <%= f.password_field :password, autocomplete: "off" %><br />
+    <%= f.label :password_confirmation %><br />
+    <%= f.password_field :password_confirmation, autocomplete: "off" %>
   </div>
 
   <div class="actions">
     <%= f.submit "Submit" %>
   </div>
 <% end %>
 
 <%= render "users/shared/links" %>
```

## 5. テスト（フィーチャスペック）を書く

さて、ここまで準備ができたらいよいよテストを書きます。
`spec/features` というディレクトリを作り、その中に `sign_up_spec.rb` というファイルを作ります。
このファイルにテストを書きます。

```ruby:spec/features/sign_up_spec.rb
require 'rails_helper'

feature 'Sign up' do
  background do
    ActionMailer::Base.deliveries.clear
  end

  def extract_confirmation_url(mail)
    body = mail.body.encoded
    body[/http[^"]+/]
  end

  scenario 'メールアドレスのみでユーザー登録を行い、パスワードを後から設定する' do
    visit root_path
    expect(page).to have_http_status :ok

    click_link 'Sign up'
    fill_in 'Email', with: 'foo@example.com'
    expect { click_button 'Sign up' }.to change { ActionMailer::Base.deliveries.size }.by(1)
    expect(page).to have_content 'A message with a confirmation link has been sent to your email address'

    mail = ActionMailer::Base.deliveries.last
    url = extract_confirmation_url(mail)
    visit url
    expect(page).to have_content 'Enter new password'

    # 登録に失敗する場合
    click_button 'Submit'
    expect(page).to have_content "Password can't be blank"
    expect(page).to have_content "Password confirmation can't be blank"

    fill_in 'Password', with: '12345678'
    fill_in 'Password confirmation', with: '123456789'
    click_button 'Submit'
    expect(page).to have_content 'Password confirmation does not match password'

    # 登録に成功する場合
    fill_in 'Password', with: '12345678'
    fill_in 'Password confirmation', with: '12345678'
    click_button 'Submit'
    expect(page).to have_content 'Your email address has been successfully confirmed.'

    click_link 'Log out'
    expect(page).to have_content 'Signed out successfully.'

    click_link 'Log in'
    fill_in 'Email', with: 'foo@example.com'
    fill_in 'Password', with: '12345678'
    click_button 'Log in'
    expect(page).to have_content 'Signed in successfully.'
  end
end
```

では、ここからテストコードの内容を順に説明していきます。

### 5-1. テストの実行前に送信メールをクリアする

詳しくは後述しますが、テストコード内で送信メールをチェックする部分があります。
予期せぬトラブルを避けるため、テスト実行前に送信メールのリストを空っぽにしておきます。

```ruby
background do
  ActionMailer::Base.deliveries.clear
end
```

ただし、これは書かなくても今回のテストコードには影響はありません。（念のため実行しているだけです）

### 5-2. トップページにアクセスする

ここからあとは `scenario` ブロック内のコードを説明していきます。

なお、フィーチャスペックを書く場合は画面の表示内容に大きく依存するので、画面とコードを同時に見ていった方が理解しやすいです。
以降の説明では「画面 => テストコード」の順に説明を入れていきます。

まず最初に、トップページにアクセスしてエラーが起きないことを検証しています。

![Screen Shot 2016-06-15 at 07.09.50.png](https://qiita-image-store.s3.amazonaws.com/0/7465/5f126661-9119-5076-dcee-12c27641bb00.png "Screen Shot 2016-06-15 at 07.09.50.png")


```ruby
# トップページにアクセス
visit root_path
# HTTPステータスコードに異常がないことを検証する
expect(page).to have_http_status :ok
```

ここではHTTPステータスコードをチェックしましたが、以下のようにページに特定の文字列が含まれることを検証するのもよいと思います。

```ruby
visit root_path
expect(page).to have_content 'You are currently logged out.'
```

### 5-3. メールアドレスを入力して、パスワード設定メールを送信する

次にメールアドレスを入力して、パスワード設定メールを送信します。

![Screen Shot 2016-06-15 at 07.10.47.png](https://qiita-image-store.s3.amazonaws.com/0/7465/466713e9-f8a2-b584-0bb6-254e56fe1f37.png "Screen Shot 2016-06-15 at 07.10.47.png")

![Screen Shot 2016-06-15 at 07.11.07.png](https://qiita-image-store.s3.amazonaws.com/0/7465/ec0a3324-2d9f-6b7e-e510-d7df96eedbf5.png "Screen Shot 2016-06-15 at 07.11.07.png")

```ruby
# Sign upのリンクをクリック
click_link 'Sign up'
# メールアドレスを入力
fill_in 'Email', with: 'foo@example.com'
# Sign upボタンをクリック。その際、送信メールのリストが1件増えることを検証する
expect { click_button 'Sign up' }.to change { ActionMailer::Base.deliveries.size }.by(1)
# 画面上に「送信成功」のメッセージが表示されていることを検証する
expect(page).to have_content 'A message with a confirmation link has been sent to your email address'
```

ポイントは以下の部分です。

```ruby
expect { click_button 'Sign up' }.to change { ActionMailer::Base.deliveries.size }.by(1)
```

RSpecの change マッチャを使って、ボタンクリック後に送信メールが1件増えていることを検証しています。
ここでエラーが出る場合は何らかの理由でメールの送信に失敗していることになります。

### 5-4. パスワード設定画面にアクセスする

**2016.6.17追記：もっとシンプルにテストが書けました！**
この項ではメール本文からURLを抜き出す方法を説明していますが、以下のようにするともっと簡単にURLを取得することができます。

```ruby
# 改善前 （メール本文からURLを抜き出す）
mail = ActionMailer::Base.deliveries.last
url = extract_confirmation_url(mail)
visit url
expect(page).to have_content 'Enter new password'

# 改善後 （Userのインスタンスからconfirmation_tokenを取り出す）
user = User.last
token = user.confirmation_token
visit users_confirmation_path(confirmation_token: token)
expect(page).to have_content 'Enter new password'
```

このあとの説明や解説動画の中では改善前のコードを説明していますが、実際にテストを書く場合は改善後のコードを使うことをオススメします。

**（追記ここまで）**

さて、おそらくここが一番特殊な部分です。
画面だけでなく、メールの送信内容も載せておきます。

```
<p>Welcome foo@example.com!</p>

<p>You can confirm your account email through the link below:</p>

<p><a href="http://localhost:3000/users/confirmation?confirmation_token=rxjBHEkJ5AvyQ7Ms1tYx">Confirm my account</a></p>
```

![Screen Shot 2016-06-15 at 08.47.58.png](https://qiita-image-store.s3.amazonaws.com/0/7465/bc979627-1797-9233-658a-e8b42d39746f.png "Screen Shot 2016-06-15 at 08.47.58.png")

```ruby
def extract_confirmation_url(mail)
  body = mail.body.encoded
  body[/http[^"]+/]
end

# 最後に送信されたメールを取得
mail = ActionMailer::Base.deliveries.last
# メール本文からパスワード設定画面のURLを抽出
url = extract_confirmation_url(mail)
# パスワード設定画面を開く
visit url
# 正しく開いたことを検証する
expect(page).to have_content 'Enter new password'
```

パスワード設定画面にアクセスするためには confirmation_token をURLに含める必要があります。
しかし、（おそらく）このtokenはランダムに発行されるため、テストコード内にtokenを決め打ちで書いておくことができません。
そこで、送信したメールからパスワード設定画面のURLを引っこ抜きます。
それをやっているのが以下のメソッドです。

```ruby
def extract_confirmation_url(mail)
  body = mail.body.encoded
  body[/http[^"]+/]
end
```

ここでは単純にメール内に含まれるURLっぽい文字列を抜き出しているだけです。
[Rubular](http://rubular.com/)で確認してみると、何をやっているのかがわかるはずです。

![Kobito.buOW6J.png](https://qiita-image-store.s3.amazonaws.com/0/7465/826821e7-01c5-9bf6-570c-56a24f1695f3.png "Kobito.buOW6J.png")

ただし、メール内に複数のURLが含まれている場合はもう少し厳密な正規表現（ `http[^"]+users\/confirmation[^"]+` など）を指定する必要が出てきそうです。

### 5-5. パスワードを設定する（失敗する場合もテストする）

パスワード設定画面にアクセスしたあとは、パスワードがちゃんと登録できるかどうかをテストします。
正常に登録できる場合だけでなく、パスワードを未入力で登録しようとした場合や、確認用パスワードが一致しない場合もテストしています。
細かい内容はコード内のコメントを参照してください。

![Screen Shot 2016-06-15 at 08.47.58.png](https://qiita-image-store.s3.amazonaws.com/0/7465/bc979627-1797-9233-658a-e8b42d39746f.png "Screen Shot 2016-06-15 at 08.47.58.png")
![Screen Shot 2016-06-15 at 08.48.17.png](https://qiita-image-store.s3.amazonaws.com/0/7465/2219638e-9b88-e9f2-42e5-3d80bbc69bbb.png "Screen Shot 2016-06-15 at 08.48.17.png")
![Screen Shot 2016-06-15 at 08.48.36.png](https://qiita-image-store.s3.amazonaws.com/0/7465/d27c965e-f6b2-b853-1b63-f0d057622a8d.png "Screen Shot 2016-06-15 at 08.48.36.png")
![Screen Shot 2016-06-15 at 07.16.18.png](https://qiita-image-store.s3.amazonaws.com/0/7465/7c263396-88f3-d199-a57d-ba25709736eb.png "Screen Shot 2016-06-15 at 07.16.18.png")

```ruby
# 登録に失敗する場合
# 何も入力せずにSubmitボタンをクリック
click_button 'Submit'
# 画面にエラーメッセージが表示されることを検証する
expect(page).to have_content "Password can't be blank"
expect(page).to have_content "Password confirmation can't be blank"

# 一致しない確認パスワードを入力する
fill_in 'Password', with: '12345678'
fill_in 'Password confirmation', with: '123456789'
# Submitボタンをクリック
click_button 'Submit'
# 画面にエラーメッセージが表示されることを検証する
expect(page).to have_content 'Password confirmation does not match password'

# 登録に成功する場合
# パスワードと確認用パスワードを正しく入力する
fill_in 'Password', with: '12345678'
fill_in 'Password confirmation', with: '12345678'
# Submitボタンをクリック
click_button 'Submit'
# パスワードの設定が正常に完了したことを検証する
expect(page).to have_content 'Your email address has been successfully confirmed.'
```

### 5-6. ログアウトとログインも検証する

ユーザー登録（Sign up）のフローを確認するだけであれば、ここまでの内容だけで十分なのですが、ついでにログアウトとログインができることも検証しておきましょう。

![Screen Shot 2016-06-15 at 07.16.18.png](https://qiita-image-store.s3.amazonaws.com/0/7465/7c263396-88f3-d199-a57d-ba25709736eb.png "Screen Shot 2016-06-15 at 07.16.18.png")
![Screen Shot 2016-06-15 at 07.17.01.png](https://qiita-image-store.s3.amazonaws.com/0/7465/45bb1f68-a306-c751-fa2b-83e05765a9ae.png "Screen Shot 2016-06-15 at 07.17.01.png")
![Screen Shot 2016-06-15 at 07.17.17.png](https://qiita-image-store.s3.amazonaws.com/0/7465/9988cf43-9e29-00f1-5c76-90781c8ff3e1.png "Screen Shot 2016-06-15 at 07.17.17.png")
![Screen Shot 2016-06-15 at 07.17.22.png](https://qiita-image-store.s3.amazonaws.com/0/7465/82ff0456-4cdc-9085-fd03-c0e6a3f72df4.png "Screen Shot 2016-06-15 at 07.17.22.png")


```ruby
# Log outリンクをクリック
click_link 'Log out'
# ログアウトメッセージが表示されていることを検証する
expect(page).to have_content 'Signed out successfully.'

# Log inリンクをクリック
click_link 'Log in'
# メールアドレスを入力
fill_in 'Email', with: 'foo@example.com'
# パスワードを入力
fill_in 'Password', with: '12345678'
# Log inボタンをクリック
click_button 'Log in'
# ログイン成功のメッセージが表示されていることを検証する
expect(page).to have_content 'Signed in successfully.'
```

これでテストコードの説明はおしまいです。

## まとめ

いかがだったでしょうか？
このようにしてテストを自動化しておけば、毎回手作業でユーザー登録のフローを確認する必要はありません。

RailsやDeviseは仕様変更が結構激しいので、バージョンを上げると突然動かなくなった！ということがしばしば発生します。
そんなときテストが自動化してあると問題をすぐに検知できるので、精神的にとてもラクになります。

みなさんもこの記事を参考にして、自分のアプリケーションにテストコードを追加してみてください。

## 参考資料

### RSpec
RSpecやCapybaraの使い方がわからん！という方はこちらの記事をご覧ください。

[使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita](http://qiita.com/jnchito/items/42193d066bd61c740612) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/42193d066bd61c740612" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/42193d066bd61c740612" class="bookmark-count"></a>

[使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita](http://qiita.com/jnchito/items/2e79a1abe7cd8214caa5) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/2e79a1abe7cd8214caa5" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/2e79a1abe7cd8214caa5" class="bookmark-count"></a>

[使えるRSpec入門・その4「どんなブラウザ操作も自由自在！逆引きCapybara大辞典」 - Qiita](http://qiita.com/jnchito/items/607f956263c38a5fec24) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/607f956263c38a5fec24" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/607f956263c38a5fec24" class="bookmark-count"></a>

上のQiita記事に加えて、Everyday Railsも読んでもらえば完璧です！（宣伝）

[Everyday Rails - RSpecによるRailsテスト入門![Screen Shot 2016-06-15 at 09.14.08.png](https://qiita-image-store.s3.amazonaws.com/0/7465/5802d72a-2002-f5ef-70a9-550dacc1185f.png "Screen Shot 2016-06-15 at 09.14.08.png")
](https://leanpub.com/everydayrailsrspec-jp/) 

### 正規表現

正規表現がわからん！という方はこちらの記事をどうぞ。

[初心者歓迎！手と目で覚える正規表現入門・その１「さまざまな形式の電話番号を検索しよう」 - Qiita](http://qiita.com/jnchito/items/893c887fbf19e17d3ff9) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/893c887fbf19e17d3ff9" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/893c887fbf19e17d3ff9" class="bookmark-count"></a>


[初心者歓迎！手と目で覚える正規表現入門・その２「微妙な違いを許容しつつ置換しよう」 - Qiita](http://qiita.com/jnchito/items/64c3fdc53766ac6f2008) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/64c3fdc53766ac6f2008" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/64c3fdc53766ac6f2008" class="bookmark-count"></a>

[初心者歓迎！手と目で覚える正規表現入門・その３「空白文字を自由自在に操ろう」 - Qiita](http://qiita.com/jnchito/items/6f0c885c1c4929092578) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/6f0c885c1c4929092578" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/6f0c885c1c4929092578" class="bookmark-count"></a>

[初心者歓迎！手と目で覚える正規表現入門・その４（最終回）「中級者テクニックをマスターしよう」 - Qiita](http://qiita.com/jnchito/items/b0839f4f4651c29da408) <a href="http://b.hatena.ne.jp/entry/http://qiita.com/jnchito/items/b0839f4f4651c29da408" target="_blank"><img src="http://b.st-hatena.com/entry/image/http://qiita.com/jnchito/items/b0839f4f4651c29da408" class="bookmark-count"></a>