Railsチュートリアル 第7章 ユーザー登録 - ユーザー登録失敗

# 正しいフォーム

## 前提条件

```html
<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post" kpxcform-initialized="true" kpxcusername="user_email" kpxcpassword="user_password">
```

Railsにより実際に構築されたHTMLのうち、`form`要素の開始タグのみを取り出したものです。タグの属性から、以下のことがわかります。

* このフォームでsubmitすると、`/users`に対してHTTPリクエストを送出する
* このフォームが送出するHTTPリクエストの種類は`POST`である

Railsの仕様上、`/users`に対する`POST`リクエストは`create`アクションによって処理されるようになっています。

## このフォームでsubmitしてからの一連の処理

1. `create`アクションで、フォームによって生成されたHTTPリクエストを受け取る
2. `User.new`メソッドによって新しいユーザーオブジェクトが生成される
3. ユーザーオブジェクトの情報をRDBMSに保存する処理を行う
4. 再度ユーザー登録ページを表示する

「ユーザーオブジェクトの情報をRDBMSに保存する」という処理は、必ず成功するとは限らず、失敗することもあります。成功時と失敗時では違う処理を実装する必要があります。

## ユーザー登録の失敗に対応できる`create`アクション

```diff:app/controllers/users_controller.rb
  class UsersController < ApplicationController

    ...略
+
+   def create
+     @user = User.new(params[:user])  # TODO:実装はまだ終わっていない
+     if @user.save
+       # TODO: 保存の成功をここに実装する
+     else
+       render 'new'
+     end
  end
```

ここで重要なポイントは以下です。

* `@user.save`は、保存が成功した場合に`true`、保存が失敗した場合に`false`を返す
* `render`メソッドは、コントローラ中でも正常に動作する

### 実際に無効なユーザー登録データを送信(submit)してみる

現時点で実際に無効なユーザー登録データを送信(submit)してみると、Webブラウザには以下のようなエラー画面が出力されます。

<img width="1011" alt="スクリーンショット 2019-10-19 14.21.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/550679b6-df92-f3e1-5df4-9c95c24cb18a.png">

`rails server`のログには、以下のようなエラーメッセージが残されています。

```
Started POST "/users" for 172.17.0.1 at 2019-10-19 05:19:50 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"wwiu2NvcdY59JxI/Eq96qLM88Oe8oAsBPMM7RQ1JT4xO47Pt+wSscw8fK40/S+DAa8h5ziATCnlV5gxUWDsiag==", "user"=>{"name"=>"", "email"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"}
Completed 500 Internal Server Error in 199ms (ActiveRecord: 41.4ms)


ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError):

app/controllers/users_controller.rb:12:in `create'
```

### HTTPリクエスト中の`user`パラメータハッシュと、それに対してRailsが行う処理

`rails server`のログから、HTTPリクエストの内容のうちの`user`パラメータハッシュだけを抜粋してみます（内容は見やすいように整形します）。

```
"user"=>{ "name"=>"",
          "email"=>"",
          "password"=>"[FILTERED]",
          "password_confirmation"=>"[FILTERED]"
        }
```

ここで重要なポイントは以下です。

* HTTPリクエストの内容は、そのものが`params`というハッシュとしてUsersコントローラに渡される
* ユーザー登録情報の送信においては、`params`ハッシュの中にさらにハッシュが含まれる(hash-of-hashes)

ここで改めて、ユーザー登録フォームの`input`要素の一つを見てみましょう。

```
<input type="email" name="user[email]" id="user_email" data-kpxc-id="user_email" kpxc-username-field="true">
```

今回着目するのは`name`属性です。実際の動作は以下のようになります。

1. HTTPリクエストが組み立てられ、フォームがsubmitされる
2. Railsは、受け取ったHTTPリクエストの内容からハッシュを構成する
    * 上述ログにおけるハッシュのキーが`"user"`などの文字列である
    * `params`ハッシュの属性名は、HTTPリクエストに存在するクエリパラメータの名前を元に構成される
    * クエリパラメータの名前は、フォームの各構成要素の`name`属性を元に構成される
3. コントローラは、`params`ハッシュ内の`:user`ハッシュを引数として`User.new`メソッドを実行する
    * ここまで「`param`ハッシュ」と言ってきたが、`param`の型は、実はRailsで定義された`ActionController::Parameters`型である
    * `ActionController::Parameters`型（およびそのスーパークラスである`ActiveSupport::HashWithIndifferentAccess`型）は、キーが文字列(`"user"`)であるかシンボル(`:user`)であるかを区別しないように作られている
    * 結果として、上述のハッシュのキーの数および名前は、`User.new`の引数で必要となるデータと完全に一致する

長くなりました。特に重要そうなポイントは以下だと思います。

* HTMLフォーム内各要素の`name`属性が、Railsでどう処理されるか
* `param`ハッシュについて
* `ActionController::Parameters`型とその特徴
* （復習）`User.new`の引数で必要となるデータ

## マスアサインメント

実は、以下2つのコードは、Railsにおいてほぼ同じ挙動をします。

```ruby:
params[:user] = { name: "Foo Bar",
                  email: "foo@invalid",
                  password: "foo",
                  password_confirmation: "bar"
                }

@user = User.new(params[:user])
```

```ruby:
@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")
```

バージョン3.xまでのRailsには、「1つのモデルに対し、`@user = User.new(params[:user])`のようにして当該モデルのフィールドをまとめて設定できる」という機能がありました。この機能をマスアサインメント機能といいます。

しかしながら、バージョン4.x以降のRailsでは、`@user = User.new(params[:user])`というようなコードを実行するとエラーになります。マスアサインメント機能には深刻な脆弱性が存在し、その対策が実装されたためです。次の項目「Strong Parameters」で解説していきます。

# Strong Parameters

## マスアサインメント機能に起因する脆弱性

しかしながら、このようなマスアサインメント系のメソッドは、無条件に全てのデータを上書きしてしまうため、「マスアサインメント機能に起因する脆弱性」というものが生まれることになってしまいました。

### どのような脆弱性か

例えば、以下のような実装がなされたRailsアプリケーションがあるとしましょう。

* Userモデルに`admin`という属性がある
* `admin`属性が`true`であるユーザーは、Webサイトの管理者権限を持つ

このような実装でマスアサインメントが可能な場合、`params[:user]`に`admin='1'`なるクエリパラメータを紛れ込ませて渡してしまえば、`admin`属性を`true`にできてしまいます。こうした属性を含む`params`ハッシュが丸ごと`User.new`に渡されてしまうと何が起こるでしょうか。「どのユーザーでも`admin='1'`をHTTPリクエストに紛れ込ませることでWebサイトの管理者権限を奪うことができる」という事態が発生してしまうのです。

### 当該脆弱性への対策

Rails 4.0では、コントローラ層でStrong Parametersという機能を使うことにより、マスアサインメント脆弱性への対策としています。Strong Parameters機能で実現できることは以下のとおりです。

* 必須のパラメータと許可されたパラメータを指定する
* `params`ハッシュを直接渡すとエラーが発生するようにする

## 現在のアプリケーションにおける、Strong Parameters機能の仕様

現在のアプリケーションでは、以下の仕様にしたいと考えています。

* `params`ハッシュにおいて、`:user`属性を必須とする
* `params`ハッシュの`:user`属性では、以下の属性を許可し、それ以外の属性は許可しない
    * 名前(`user[:name]`)
    * メールアドレス(`user[:email]`)
    * パスワード(`user[:password]`)
    * パスワードの確認(`user[:password_confirmation]`)

上述仕様に対応するメソッドチェーンは以下となります。

`params.require(:user).permit(:name, :email, :password, :password_confirm)`

このメソッドチェーンの戻り値は、許可された属性のみが含まれた`params`のハッシュです（`:user`属性がない場合はエラー）。

上記動作をまとめると以下の通りです。

* `params.require`の引数にシンボルを与えることにより、`params`ハッシュに当該シンボルをキーとするハッシュが含まれていることが必須となる
* `params.require(:symbol).permit`の引数にシンボルを与えることにより、`params[:symbol]`ハッシュに以下の条件が定義される
    * `params.require(:symbol).permit`の引数に与えられたシンボルをキーとするハッシュ値が含まれることを許可する
    * `params.require(:symbol).permit`の引数に与えられたシンボル以外をキーとするをハッシュ値が含まれることを許可しない

## `user_params`メソッドと、Rubyの`private`キーワード

`params.require(:user).permit(:name, :email, :password, :password_confirm)`

これはいかにも長いメソッド呼び出しです。そのままでは使いにくいです。こうした長いメソッド呼び出しをより使いやすくするため、Railsでは`user_params`[^user_params]という外部メソッドを使うのが慣習になっています。

`user_params`は、Strong Parameters機能が有効な環境で、`params[:user]`の代わりに使われる外部メソッドです。Strong Parameters機能で適切に初期化されたハッシュを返します。

[^user_params]: より一般化すると`[モデル名]_params`です。

### Rubyの`private`キーワード

今回実装する`user_params`メソッドは、もっぱらUsersコントローラの内部でのみ使われ、Web経由で外部ユーザにさらされる必要はありません。そのため、Railsの`private`キーワードによって外部から使えないように制限します。

## `create`アクションで実際にStrong Parameters機能を活用する

```diff:app/controllers/users_controller.rb
  class UsersController < ApplicationController

    ...略

    def create
-     @user = User.new(params[:user])  # TODO:実装はまだ終わっていない
+     @user = User.new(user_params)
      if @user.save
        # TODO: 保存の成功をここに実装する
      else
        render 'new'
      end
    end
+
+   private
+
+     def user_params
+       params.require(:user).permit(:name, :email, :password, password_confirmation)
+     end
  end
```

なお、Railsチュートリアル本文では、「`private`キーワード以降のコードを強調するために、`user_params`のインデントを1段深くしていること」について、ここでより突っ込んだ解説をしています。以下の理由でよろしいのだとか。

* クラス内に多数のインデントがある場合、privateメソッドの場所を簡単に見つけられるようになる
    * インデントがない場合に比べ、どこからprivateになるのかについて困惑しなくなる

ここまでの実装が完了すれば、「送信ボタンを押してもエラーが出ないユーザー登録フォーム」が実現します。しかしながら、現在のところ、（開発者用のデバッグ領域を除いて）間違った情報を送信しても何もフィードバックは返ってきません。現状で間違った情報を送信した後のWebブラウザの表示内容は以下のようになります。

<img width="1161" alt="スクリーンショット 2019-10-20 21.47.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/f561bd4d-83fe-8515-5f5e-b3ff59c865de.png">

デバッグ領域の内容を見れば「間違った情報が送られた」ということはわかるのですが、それ以外は新規にユーザー登録画面を開いた場合と何ら変わりありません。これでは利用者が困惑することは避けられません。

さらに、有効なユーザー情報を送信しても新しいユーザーが実際に作成されることはありません。

以上の問題は、今後の実装にて解決していきます。

# 演習 - Strong Parameters

## 1. /signup?admin=1 にアクセスし、`params`の中に`admin`属性が含まれていることをデバッグ情報から確認してみましょう。 

`http://localhost:8080/signup?admin=1`にアクセスした場合、デバッグ領域の内容は以下のようになります。

<img width="1178" alt="スクリーンショット 2019-10-20 21.56.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/f9c8c900-89a5-f170-3d4a-665f664018c8.png">

確かに`admin: '1'`が含まれていますね。

なお、このときの`rails server`のログは以下のようになります。

```
Started GET "/signup?admin=1" for 172.17.0.1 at 2019-10-20 12:55:02 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#new as HTML
  Parameters: {"admin"=>"1"}
  ...略
Completed 200 OK in 951ms (Views: 900.6ms | ActiveRecord: 0.0ms)
```

同様に、例えば /signup?foobar=1 にアクセスすると、`params`の中には`foobar`属性が含まれます。

# エラーメッセージ

## Railsによるエラーメッセージの自動生成

Railsは、モデルオブジェクトの検証でエラーが発生した場合、発生したエラーに対応するエラーメッセージを自動で生成する機能を有しています。

```
# rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?>                 password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
```

以上は、「無効なメールアドレス、かつ、短すぎるパスワード」というUserモデルのオブジェクトを保存しようとした場合の経過です。`user.save`が失敗した時点で、`@user`オブジェクトに関連付けられたエラーメッセージの配列が`errors.full_messages`にされます。

## Railsによって自動生成されたエラーメッセージをRailsアプリケーションで使う方法

`@user`オブジェクトに関連付けられたエラーメッセージをRailsアプリケーションで使う、例えばエラーメッセージをWebブラウザに表示することももちろん可能です。今回は、「ユーザーの`new`ページでエラーメッセージのパーシャル(partial)を出力する」という方法をとります。

```diff:app/views/users/new.html.erb
  <% provide(:title, 'Sign up') %>
  <h1>Sign up</h1>

  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user) do |f| %>
+       <%= render 'shared/error_messages' %>
+
        <%= f.label :name %>
-       <%= f.text_field :name %>
+       <%= f.text_field :name, class: 'form-control' %>

        <%= f.label :email %>
-       <%= f.email_field :email %>
+       <%= f.email_field :email, class: 'form-control' %>

        <%= f.label :password %>
-       <%= f.password_field :password %>
+       <%= f.password_field :password, class: 'form-control' %>

        <%= f.label :password_confirmation, "Confirmation" %>
-       <%= f.password_field :password_confirmation %>
+       <%= f.password_field :password_confirmation, class: 'form-control' %>

        <%= f.submit "Create my account", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
```

`class: 'form-control'`というコードは、「Bootstrapがうまく扱えるようにクラスを追加する」という意味のコードです。

上述コードにおける主要な変更内容は、「`shared/error_messages`というパーシャルを描画(render)する機能を実装した」という点です。より細かく見ていくと、以下のような点が重要です。

* 複数のビューで使われるパーシャルは、専用のディレクトリである`shared`に置かれる
* 現時点で`app/views/shared`というディレクトリは存在しないので、新規にディレクトリを作成する必要がある
* `app/views/shared/_error_messages.html.erb`というファイルを新規に作成する必要がある
    * `error_messages`は部分テンプレートなので、ファイル名の頭には`_`が必要である


## フォーム送信時にエラーメッセージを表示するためのパーシャルと、その解説

早速必要なディレクトリとファイルを生成してみましょう。

```
>>> pwd
~/docker/rails_tutorial_test/sample_app
>>> mkdir app/views/shared
>>> touch app/views/shared/_error_messages.html.erb
```

```erb:app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
```

`app/views/shared/_error_messages.html.erb`には、色々と興味深い新要素が登場します。

### `errors.count`メソッド

```
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?>                 password: "dude", password_confirmation: "dude")
>> user.save
=> false

>> user.errors.count
=> 2
```

`errors.count`メソッドは、エラーの数を返すメソッドです。`errors`に限らず、配列一般に対し、Ruby標準のメソッドとして`count`メソッドが定義されています。

### `any?`メソッド

```
>> user.errors.any?
=> true
>> user.errors.empty?
=> false
```

Enumerableモジュールが実装されたオブジェクト（配列やハッシュ等）に対して定義される、Ruby標準のメソッドです。

* レシーバ[^レシーバ]に一つでも戻り値が`true`となる値が格納されていれば`true`
* レシーバが空である場合、戻り値は`false`
* レシーバに値が格納されていても、その全ての戻り値が`false`となる場合、戻り値は`false`

[^レシーバ]: 今回は`user.errors`ですね。

Railsチュートリアル本文には、「`empty?`メソッドと`any?`メソッドは互いに補完する」「`any?`メソッドはちょうど`empty?`と逆の動作で、要素が1つでもある場合は`true`、ない場合は`false`を返す」とありますが、実際には「`empty?`も`any?`も戻り値が`false`になる場合」というのが存在します。例えば以下のような場合です。

```
>> [false, false, nil, nil].any?
=> false
>> [false, false, nil, nil].empty?
=> false
```

「`empty?`も`any?`も戻り値が`false`になるのは、全ての要素が`false`または`nil`のいずれかである場合」ですね。

Railsのエラーオブジェクトに限れば、その性質上「全ての要素が`false`または`nil`のいずれかになる」という事態は発生しないはずなので、「`any?`メソッドは、要素が1つでもある場合は`true`、ない場合は`false`を返す」という認識で構わないでしょう。

### `helper.pluralize`メソッド

こちらはRailsのActiveSupportライブラリで定義されているメソッドです。第1引数に整数を、第2引数に文字列を取ります。第1の引数に基づき、第2引数（英単語であることが前提です）が単数形になるか複数形になるかを判断した上で、必要なら複数形に変換し、文字列を出力します。

`rails console`から動作を試してみることもできます。

```
>> helper.pluralize(1, "error")
=> "1 error"
>> helper.pluralize(3, "error")
=> "3 errors"
```

今回のコードでは、`pluralize`が以下のように使われています。

```erb:
<%= pluralize(@user.errors.count, "error") %>
```

その結果は、`@user.errors.count`の内容に応じ、例えば`"0 errors"`,`"1 error"`,`"2 errors"`のようになります。結果として、`"1 errors"`のような英文法に合わないメッセージが出現する事態を自動的に避けることが可能になります。

### 文字列に対する`pluralize`メソッド

RailsのActiveSupportライブラリでは、`helper.pluralize`のみならず、一般の文字列に対する`pluralize`メソッドも定義されています。レシーバーとして与えられた文字列を（英単語であることを前提として）複数形に変換します。

```
>> "error".pluralize
=> "errors"
>> "query".pluralize
=> "queries"
```

不規則活用の名詞を複数形にすることもできます。

```
>> "woman".pluralize
=> "women"
>> "erattum".pluralize
=> "eratta"
>> "person".pluralize
=> "people"
```

### 文字列に対する`singularize`メソッド

RailsのActiveSupportライブラリでは、`singularize`メソッドも定義されています。

なお、`helper.singularize`は定義されていないようです。`pluralize`メソッドとは逆に、レシーバーとして与えられた文字列を（英単語であることを前提として）単数形に変換します。

```
>> "errors".singularize
=> "error"
>> "women".singularize
=> "woman"
>> "people".singularize
=> "person"
```

### `pluralize`や`singularize`に関する注意事項

`pluralize`や`singularize`といったメソッドは、`ActiveSupport::Inflector`というライブラリにて定義されています。

こうした便利なメソッドの恩恵を享受するためには、変数やメソッドなどの名前は英語で定義されている必要があります。Railsに限らず、純粋なRubyも英語を強く意識した言語仕様になっています。特にRails開発においては、`pluralize`や`singularize`によって単数形と複数形を変換して使うのが前提になっているので、変数やメソッドの名前を英語以外の言語で定義するのは避ける必要があります。

## エラーメッセージにスタイルを与えるためのCSS

### 使用可能なCSSセレクタ

改めて`app/views/shared/_error_messages.html.erb`には、`error_explanation`というCSS IDを持つ`div`要素が含まれています。

```erb:app/views/shared/_error_messages.html.erb
<div id="error_explanation">
...略
</div>
```

またRailsは、「無効な内容の送信によって元のページに戻される」という動作が発生すると、エラーに関するHTMLの内容のみを、クラス`field_with_errors`が定義された`div`要素で自動的に囲うという動作をします。

結果、エラーメッセージにスタイルを与えるためには、以下2つのCSSセレクタを使用できることになります。

* `error_explanation`というCSS ID
* `field_with_errors`というCSSクラス

### 実際にSCSSを書いてみる

```diff:app/assets/stylesheets/custom.scss
  @import "bootstrap-sprockets";
  @import "bootstrap";
  ...略
  /* forms */
  ...略
+
+ #error_explanation {
+     color: red;
+     ul {
+         color: red;
+         margin: 0 0 30px 0;
+     }
+ }
+
+ .field_with_errors {
+     @extend .has-error;
+     .form_control {
+         color: $state-danger-text;
+     }
+ }
  ...略 
```

このコードのポイントは「Sassの`@extend`関数により、Bootstrapの`has-error`というCSSクラスが適用されている」という点です。`$state-danger-text`というのもBootstrap由来ですね。

## ここまで実装してきたものを踏まえ、改めてエラーメッセージをWebブラウザで表示させてみる

ここまでに、以下の実装を行ってきました。

* Userモデルの`errors`オブジェクトを元に、エラーの内容を得られるようにした
* ユーザー登録が失敗した場合に、以下の情報が表示されたユーザー登録画面が出力されるようにした
    * エラーが発生した事実
    * 入力内容の何が悪くてエラーが発生したのか
* SCSSにより、ユーザー登録画面に表示されるエラーメッセージの見栄えを整えた

以上の実装を踏まえた上で、再びユーザー登録を失敗させてみましょう。

<img width="1178" alt="スクリーンショット 2019-10-21 16.07.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/a9f82ce9-649f-2d81-b418-5fa77b2649c7.png">

エラーメッセージがきちんと表示されていますね。全てのエラーをfixすれば、RDBに問題なく保存できる形のデータになりそうです。エラーメッセージの見栄えも整っています。

### 同じエラーメッセージが2回表示されてしまう問題

しかしながら、上記スクリーンショットにおいては、「Password can't be blank」というエラーメッセージが2回表示されています。原因は、以下2つのバリデーションで、両方とも空のパスワード(`nil`)を検知してしまうためです。

* `presence: true`によるバリデーション
* `has_secure_password`によるバリデーション

この問題を解決するには、今後のRailsチュートリアルで登場する`allow_nil: true`というオプションを追加すればよいのだそうです。

# 演習 - エラーメッセージ

## 1. 最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。

「最小文字数を変更する」というのは、`app/models/user.rb`を変更するのでしたね。以下の変更を加えます。

```diff:app/models/user.rb
  class User < ApplicationRecord
    ...略
-   validates :password, presence: true, length: { minimum: 6 }
+   validates :password, presence: true, length: { minimum: 5 }
  end
```

結果は以下のように変化します。確かに「minimum is 5 characters」というエラーメッセージが見受けられますね。

<img width="1178" alt="スクリーンショット 2019-10-21 16.43.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/48d221f2-b956-b960-11fc-28b113c109d2.png">

## 2. 未送信のユーザー登録フォーム ([図 7.12](https://railstutorial.jp/chapters/sign_up?version=5.1#fig-signup_form)) のURLと、送信済みのユーザー登録フォーム ([図 7.18](https://railstutorial.jp/chapters/sign_up?version=5.1#fig-signup_error_messages)) のURLを比べてみましょう。なぜURLは違っているのでしょうか? 考えてみてください。

前提条件は以下です。

* 図 7.12では、URLが`/signup`となっている
* 図 7.18では、URLが`/users`となっている

簡潔に言えば理由は以下です。

* 図 7.12は、`/signup`に`GET`リクエストを送出した結果である
* 図 7.18は、`/users`に`POST`リクエストを送出した結果である

`/users`に`POST`リクエストを送出した結果が「ユーザー登録に失敗」であった場合、Usersコントローラにより「`new`ビューが`render`される」という処理が行われます。結果として、エラーメッセージが表示されている以外は`/signup`に`GET`リクエストを送出した結果と同じものが表示されることになるのです。

# 失敗時のテスト

* ブラウザでフォームを表示する
* 有効なデータと無効なデータを交互に流し込む
* どちらの場合にもアプリケーションが正常に動作することを確認する
    * 多くの場合、Excel様式にスクリーンショットを貼り付けていく
* アプリケーションに変更が生じるたびに、まったく同じテストを繰り返す

このような「人海戦術でカバー」式のやり方は、はっきり言って担当者にとって苦痛なものです。しかしながら、Railsをはじめとする現代のWebフレームワークでは、こうしたフォームのテストをも自動化することが可能なのです。

## 新規ユーザー登録用の統合テストを作成する

開発環境で`rails generate integration_test users_signup`というコマンドを実行します。

* 統合テストなので`integration_test`
* 新規ユーザー登録用のテストなので`users_signup`
    * 「リソース名は複数形」というRailsの慣習に従った名前である

```
# pwd
/var/www/sample_app
# rails generate integration_test users_signup
Running via Spring preloader in process 204
      invoke  test_unit
      create    test/integration/users_signup_test.rb
```

今実装するのは「失敗時のテスト」ですが、今後実装する「成功時のテスト」にも、今回生成したファイルを使っていきます。

## 今回実装するテストで確認するもの

今回実装するテストは、ユーザー登録ボタンを押したときに（ユーザー情報が無効であるため）ユーザーが作成されないことを確認するものです。また、今後うっかり要素を変更してしまっても気付けるようにするために、ユーザー登録フォーム内に存在するHTML要素についてもテストしていきます。

### `User.count`メソッドを使用する

ユーザーが作成されないことを確認するための方法としては、「`User.count`メソッドを用いて、RDB上に存在するユーザーの数をカウントする」という方法を取ります。

```
>> User.count
   (2.3ms)  SELECT COUNT(*) FROM "users"
=> 1
```

### `assert_select`メソッドを使用する

HTML要素をテストするためには、「`assert_select`メソッドを使用する」という方法を取ります。

## テストの内容について解説

### `get`メソッドを使ってユーザー登録ページにアクセスする

ユーザー登録ページにアクセスするために、まず`signup_path`に対して`get`メソッドを実行します。`/signup`に`GET`リクエストを実行するのと同じ操作ですね。

```
get signup_path
```

ルーティングが正しく書かれていれば、Usersコントローラの`new`メソッドが実行され、最終的にユーザー登録ページが返ってくるはずです。

### `users_path`に`POST`リクエストを送信する

新規ユーザー登録に際してユーザー登録フォームをsubmitしたときの動作は、「`users_path`に`POST`リクエストを送信する」というものでした。この動作をテストするには、`post`メソッドを含む以下のようなコードで実現できます。

```ruby:
assert_no_difference 'User.count' do
  post users_path, params: { user: { name:  "",
                                     email: "user@invalid",
                                     password:              "foo",
                                     password_confirmation: "bar" } }
end
```

ブロック内のコードで重要なポイントは以下です。

* `post`の引数
    * 第1引数には、`POST`リクエストを送信する先のパスが与えられる
    * 第2引数には、`POST`リクエストの内容が与えられる
* `post`の第2引数に、`params[:user]`ハッシュを与えている
    * Rails 4.2以前では、`params`を暗黙的に省略しても正常に動作した
        * `user`ハッシュのみでも正しく動作する、ということ
    * Rails 5.0以降は、`params`を省略することは非推奨とされている
        * `params`ハッシュを明示的に含めることが推奨される
* `params[:user]`において、ユーザー登録情報として正しくない内容になるようなハッシュを与えている
    * `:name`の値が空である
    * `:email`の値が、メールアドレスとして正しくない文字列である
    * `:password`、`:password_confirm`共に短すぎる
    * `:password`と`:password_confirm`が一致していない

`assert_no_difference 'User.count'`については次の項で解説します。

### `assert_no_difference 'User.count'`とは

```ruby:
assert_no_difference 'User.count' do
  # ...略
end
```

`assert_no_difference`というのは、「ブロックの実行前後で引数（今回は`User.count`）の値が変化しないことをテストする」というメソッドです。結果、このテスト全体としては、以下のコードと等価になります。

```ruby:
before_count = User.count
post user_path, ...
after_count = User.count
assert_equal before_count, after_count
```

しかしながら、`assert_no_difference`のほうが明確な記法ですし、Rubyの慣習にも従った記法となります。

### ユーザー登録は、技術的にはユーザー登録フォームを経由しなくても可能である

`assert_no_difference`ブロック内では、`get`メソッドを使用していません。これは以下のことを意味します。

* `get`、`post`、`assert`等の各メソッドに技術的な関連性がない
* ユーザー登録フォームを経由せず、直接`post`メソッドを呼び出してユーザー登録を行うことが可能である

ただ、（実際の手順にならって）`get`と`post`の両メソッドをを呼び出すことには、以下の理由から十分な価値があります。

* テストのコンセプトを明確にするため
* ユーザー登録ページそのものを改めてチェックするため

## 実際にテストを実装する

### `test/integration/users_signup_test.rb`の初期状態

生成されたばかりの`test/integration/users_signup_test.rb`は、以下の内容になっています。

```ruby:test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end
```

### 実際にテストを実装したときの`test/integration/users_signup_test.rb`の内容

```ruby:test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name: "",
                                        email: "user@invalid",
                                        password:              "foo",
                                        password_confirmation: "bar" } }
    end
    assert_template 'users/new'
  end
end
```

Railsチュートリアル本文にある以下の記述には注意が必要です。

> 送信に失敗したときにnewアクションが再描画されるはずなので、assert_templateを使ったテストも含めている

ここまでの学習で、このテストが通るために必要な機能は全て実装してきました。そのため、このテストは問題なく通ります。

```
# rails test integration
Running via Spring preloader in process 250
Started with run options --seed 50920

  20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.51197s
20 tests, 41 assertions, 0 failures, 0 errors, 0 skips
```

# 演習 - 失敗時のテスト

## 1. [リスト 7.20](https://railstutorial.jp/chapters/sign_up?version=5.1#code-f_error_messages)で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。

> [リスト 7.25](https://railstutorial.jp/chapters/sign_up?version=5.1#code-error_messages_test)にテンプレートを用意しておいたので、参考にしてください。

```diff:
  require 'test_helper'

  class UsersSignupTest < ActionDispatch::IntegrationTest

    test "invalid signup information" do
      ...略
      assert_template 'users/new'
+     assert_select 'div#error_explanation'
+     assert_select 'div.alert' do
+       assert_select 'div.alert-danger'
+     end
+     assert_select 'ul'
+     assert_select 'li'
    end
  end
```

私は以下のテストを追加してみました。

* `error_explanation`というIDを持つ`div`要素が存在する
* `alert`と`alert-danger`両方のクラスを持つ`div`要素が存在する
* `ul`要素が存在する
* `li`要素が存在する

`ul`と`li`というのは、「実際にユーザーデータの不適合の内容が1つ以上表示されているか」をテストするためのアサーションです。

なお、「`alert`と`alert-danger`両方のクラスを持つ`div`要素が存在することをテストする」というのは、初見では方法がわからないかと思います。[Rails上におけるテストで、「1つのHTML要素に対し、複数のクラスが定義されていること」をテストする正しい書き方 - Qiita](https://qiita.com/rapidliner00/items/1b9a5d046e701435682c)にて、その点について解説を入れてみました。

## 2.1. ユーザー登録フォームのURLは /signup ですが、無効なユーザー登録データを送付するとURLが /users に変わってしまいます。[リスト 7.26](https://railstutorial.jp/chapters/sign_up?version=5.1#code-post_signup)と[リスト 7.27](https://railstutorial.jp/chapters/sign_up?version=5.1#code-post_signup_form)の内容を参考に、この問題を解決してみてください。

> これは[リスト 5.43](https://railstutorial.jp/chapters/filling_in_the_layout?version=5.1#code-signup_route)で追加した名前付きルート (/signup) と、RESTfulなルーティング ([リスト 7.3](https://railstutorial.jp/chapters/sign_up?version=5.1#code-users_resource)) のデフォルト設定との差異によって生じた結果です。

そういえば、演習 - エラーメッセージの設問2にもこの点についての言及が出てきましたね。

まず、`config/routes.rb`を編集し、/signup に対する`POST`リクエストでUsersコントローラの`create`アクションが実行されるようにします。

```diff:config/routes.rb
  Rails.application.routes.draw do
    get 'users/new'

    root 'static_pages#home'
    get '/help', to: 'static_pages#help'
    get '/about', to: 'static_pages#about'
    get '/contact', to: 'static_pages#contact'
    get '/signup', to: 'users#new'
+   post '/signup', to: 'users#create'
    resources :users
  end
```

続いて、 /signup に対応するビューである`app/views/users/new.html.erb`を編集し、フォームが`POST`リクエストを送出する先が /signup になるようにします。

```diff:app/views/users/new.html.erb
  <% provide(:title, 'Sign up') %>
  <h1>Sign up</h1>
  
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
-     <%= form_for(@user) do |f| %>
+     <%= form_for(@user, url: signup_path) do |f| %>     
        ...略
      <% end %>
    </div>
  </div>
```

## 2.2. （2.1. の続きです）うまくいけばどちらのURLも /signup になるはずです。あれ、でもテストは`green`のままになっていますね...、なぜでしょうか? (考えてみてください)

```
# rails test integration
Running via Spring preloader in process 363
Started with run options --seed 26654

  20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.13368s
20 tests, 46 assertions, 0 failures, 0 errors, 0 skips
```

この時点においても、テストは確かに通ります。

私としては、この場合にテストが問題なく通るのは「少なくともRESTfulなアーキテクチャを前提とした場合、『`GET`と`POST`（あるいは`PATCH`や`DELETE`でも）で、URLが同じだが、Webアプリケーションで実際に呼び出される処理が異なる』というのは、仕組み上正しい挙動であるから」というのが理由と考えています。

## 3. [リスト 7.25](https://railstutorial.jp/chapters/sign_up?version=5.1#code-error_messages_test)の`post`部分を変更して、先ほどの演習課題で作った新しいURL (/signup) に合わせてみましょう。また、テストが greenのままになっている点も確認してください。

変更するファイルは`test/integration/users_signup_test.rb`です。

```diff:test/integration/users_signup_test.rb
  require 'test_helper'

  class UsersSignupTest < ActionDispatch::IntegrationTest

    test "invalid signup information" do
      get signup_path
      assert_no_difference 'User.count' do
-       post users_path, params: { user: { name: "",
+       post signup_path, params: { user: { name: "",
                                          email: "user@invalid",
                                          password:              "foo",
                                          password_confirmation: "bar" } }
      end
      ...略
    end
  end
```

改めてテストを実行してみましょう。

```
p# rails test integration
Running via Spring preloader in process 400
Started with run options --seed 64185

  20/20: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.33716s
20 tests, 46 assertions, 0 failures, 0 errors, 0 skips
```

テストは問題なく通りますね。

## 4. [リスト 7.27](https://railstutorial.jp/chapters/sign_up?version=5.1#code-post_signup_form)のフォームを以前の状態 ([リスト 7.20](https://railstutorial.jp/chapters/sign_up?version=5.1#code-f_error_messages)) に戻してみて、テストがやはり`green`になっていることを確認してください。

> これは問題です! なぜなら、現在postが送信されているURLは正しくないのですから。

`app/views/users/new.html.erb`を以下のように変更した場合にテストは通るか、という話です。

```diff:app/views/users/new.html.erb
  <% provide(:title, 'Sign up') %>
  <h1>Sign up</h1>
  
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
-     <%= form_for(@user, url: signup_path) do |f| %>     
+     <%= form_for(@user) do |f| %>
        ...略
      <% end %>
    </div>
  </div>
```

現状でテストを実行してみましょう。

```
# rails test integration
Running via Spring preloader in process 452
Started with run options --seed 53403

  20/20: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.06687s
20 tests, 46 assertions, 0 failures, 0 errors, 0 skips
```

テストが通ってしまいました。

### 現状でテストが通ることの何が問題か

何が問題かといいますと…

```html:
<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post">
...略
</form>
```

上記`form`要素の属性において、`action`属性の値が`/users`になっていることが問題なのです。`action`属性の値は、本来は`/signup`でなければなりません。現状ではテストが通ってしまうことが逆に問題となります。

## 4.2. `assert_select`を使ったテストを[リスト 7.25](https://railstutorial.jp/chapters/sign_up?version=5.1#code-error_messages_test)に追加し、このバグを検知できるようにしてみましょう (テストを追加して`red`になれば成功です)。

> ヒント: フォームから送信してテストするのではなく、`'form[action="/signup"]'`という部分が存在するかどうかに着目してテストしてみましょう。

`test/integration/users_signup_test.rb`を以下のように変更します。

```diff:test/integration/users_signup_test.rb
  require 'test_helper'

  class UsersSignupTest < ActionDispatch::IntegrationTest

    test "invalid signup information" do
      get signup_path
+     assert_select 'form[action="/signup"]'
      ...略
    end
  end
```

ユーザー登録フォームのHTMLの内容が問題なので、/signup に対して`get`メソッドを実行した直後がテストを行う適切なタイミングになりますね。そのため、`post`メソッドを実行する前に当該テストを実行するようにしています。

改めてテストを実行してみましょう。

```
# rails test integration
Running via Spring preloader in process 478
Started with run options --seed 25723

 FAIL["test_invalid_signup_information", UsersSignupTest, 2.1218616999976803]
 test_invalid_signup_information#UsersSignupTest (2.12s)
        Expected at least 1 element matching "form[action="/signup"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:7:in `block in <class:UsersSignupTest>'

  20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.12470s
20 tests, 40 assertions, 1 failures, 0 errors, 0 skips
```

めでたく（？）テストが通らなくなりました。`Expected at least 1 element matching "form[action="/signup"]", found 0.`という内容の指摘事項なので、実装したいテストがきちんと実装されていることになりますね。

## 4.3. その後、変更後のフォーム ([リスト 7.27](https://railstutorial.jp/chapters/sign_up?version=5.1#code-post_signup_form)) に戻してみて、テストが`green`になることを確認してみましょう。

`app/views/users/new.html.erb`を以下のように変更します。

```diff:app/views/users/new.html.erb
  <% provide(:title, 'Sign up') %>
  <h1>Sign up</h1>
  
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
-     <%= form_for(@user) do |f| %>
+     <%= form_for(@user, url: signup_path) do |f| %>     
        ...略
      <% end %>
    </div>
  </div>
```

改めてテストを実行してみましょう。

```
# rails test integration
Running via Spring preloader in process 491
Started with run options --seed 19848

  20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.41956s
20 tests, 47 assertions, 0 failures, 0 errors, 0 skips
```

無事にテストが通りました。