0
0

More than 3 years have passed since last update.

Railsチュートリアル 第10章 ユーザーの更新・表示・削除 - ユーザーを更新する

Posted at

そもそもこの章で何をするか

この章で新たに実装する要素は以下です。

  • Usersリソースの以下のアクション
    • edit
    • update
    • index
    • destroy

「ユーザーを更新する」というセクションでは、User#editの機能を実装し、「ログインユーザーが自身の登録情報を編集する」ことを可能にします。

ユーザー情報更新

実装全体の流れは、新規ユーザー登録の流れと類似しています。

  1. editビューを実装する
  2. PATCHリクエストに応答するupdateアクションを実装する
実装内容 新規ユーザー登録時 ユーザー情報更新時
ビューを出力するアクション new edit
使用するHTTPリクエスト POST PATCH
HTTPリクエストに応答するアクション create update

大きな相違点は、「新規ユーザー登録は誰でも可能だが、ユーザー情報更新は当該ユーザーのみが可能である」ということです。「当該ユーザーのみが可能」ということは、当然アクセス制御が必要になります。その実装には、RDBの更新のみならず、RDBの読み込みや認証等の機能も絡んできます。

Railsにおいては、認証機能が実装されているならば、beforeフィルターによってアクセス制御が可能です。

編集フォーム

ユーザー情報の編集には、まず編集フォームが必要となります。Railsチュートリアル本文においては、編集フォームのモックアップとして、図 10.1が示されています。

editアクションの定義

editアクションを定義せずにビューだけ作ってしまうと…

以下のスクリーンショット・ログのようなエラーが出てしまいます。

後述するビューの定義において、@userには有効なUserモデルの実体が必要とされます。しかしながら、editアクションがなければ、@userに有効なUserモデルの実体は与えられません。したがって、以下のようなエラーになります。

スクリーンショット 2019-11-15 7.51.24.png

Started GET "/users/2/edit" ...略
Processing by UsersController#edit as HTML
  Parameters: {"id"=>"2"}
  Rendering users/edit.html.erb within layouts/application
  Rendered users/edit.html.erb within layouts/application (16.4ms)
Completed 500 Internal Server Error in 68ms (ActiveRecord: 0.0ms)

ActionView::Template::Error (First argument in form cannot contain nil or be empty):
    3: 
    4: <div class="row">
    5:   <div class="col-md-6 col-md-offset-3">
    6:     <%= form_for(@user) do |f| %>
    7:       <%= render 'shared/error_messages' %>
    8: 
    9:       <%= f.label :name %>

app/views/users/edit.html.erb:6:in `_app_views_users_edit_html_erb__939350102988132727_69984448042620'

実際にeditアクションを定義する

Userリソースの /edit (例えば /users/1/edit)に対するGETリクエストに対して@userの内容を定義するためには、app/controllers/users_controller.rbeditアクションに定義を記述していきます。HTTPリクエストから対象ユーザーのIDを取り出すには、params[:id]変数を使うのでしたね。

app/controllers/users_controller.rb
  class UsersController < ApplicationController

    ...略

+   def edit
+     @user = User.find(params[:id])
+   end

    private  ...略
  end

HTMLを生成するための埋め込みRubyのソース

Usersリソースのeditに対応するビューは、touch app/views/users/edit.html.erbというファイルに記述していきます。現時点で当該ファイルは存在しないため、まず当該ファイルを生成する必要があります。ファイルの生成はtouchコマンドですね。

>>> touch app/views/users/edit.html.erb

app/views/users/edit.html.erbの内容は、以下の通りとなります。

app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</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, class: 'form_control' %>

      <%= f.label :email %>
      <%= f.text_field :email, class: 'form_control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form_control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form_control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

ユーザー新規作成フォームと同様、以下のようなフィールドをもつ入力フォームを定義しています。

  • 名前の入力フィールド
  • メールアドレスの入力フィールド
  • パスワードの入力フィールド
  • パスワードの確認の入力フィールド
  • submitボタン

また、この他に、Grabatarへのリンクも挿入されています。

編集ページの描画

ここまで実装した時点でユーザー編集ページをWebブラウザで表示すると、その描画結果は以下のようになります。

スクリーンショット 2019-11-15 8.02.26.png

編集画面における名前やメールアドレスは、Railsによって既存のUserリソースから読み出された値が自動で入力されます。

Railsの内部処理について - フォーム内容が酷似する新規ユーザー作成時と既存ユーザー編集時の比較から

PATCHリクエストをどう発行する?

RESTアーキテクチャにおいて、既存リソースの内容を更新する際には、POSTメソッドではなくPATCHメソッドが発行されることが要求されます。

一方で、2019年11月現在、HTMLフォームが発行できるHTTPメソッドは、GETまたはPOSTのみです。RESTで要求されるPUTPATCHDELETEといったHTTPメソッドは、HTMLフォームのみでは発行できません。

HTMLフォームのみでは発行できないHTTPメソッドを発行したい場合、Webフレームワーク側で対応することになります。Railsにおいては、「POSTリクエストと隠しinputフィールドにより、PATCHリクエストを"偽造"する」という内部実装が行われています。

<input name="_method" type="hidden" value="patch" />

新規ユーザー用のPOSTリクエストと既存ユーザー用のPATCHリクエスト。Railsは内部でどう区別する?

少なくともRailsチュートリアルにおいて、新規ユーザー用のPOSTリクエストと既存ユーザー用のPATCHリクエストの内容は全く同じになります。とすると、Railsはこの2つのリクエストをどう区別するのでしょうか。

このようなリクエストが送られてきた場合、Railsは内部でActiveRecordのnew_record?メソッドを呼び出します。その結果がtrueであればPOSTfalseであればPATCHと判断する、という次第です。

#rails console --sandbox

>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

ナビゲーションバーの内容を更新する

ユーザー編集ページを実装したので、ナビゲーションバーの内容も更新する必要があります。

<%= link_to "Settings", edit_user_path(current_user) %>

この埋め込みRubyでは、以下の技術を用いています。

  • 名前付きルートedit_user_path
  • current_userヘルパー

実装箇所はapp/views/layouts/_header.html.erbです。以前との差分を以下に記述します。

app/views/layouts/_header.html.erb
  <header class="navbar navbar-fixed-top navbar-inverse">
    <div class="container">
      ...略
      <nav>
        <ul class="nav navbar-nav navbar-right">
          ...略
          <% if logged_in? %>
            ...略
            <li class="dropdown">
              ...略
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
-               <li><%= link_to "Settings", '#') %></li>
+               <li><%= link_to "Settings", edit_user_path(current_user) %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Log out", logout_path, method: :delete %>
                </li>
              </ul>
            </li>
          <% else %>
            ...略
          <% end %>
        </ul>
      </nav>
    </div>
  </header>

演習 - 編集フォーム

1. Gravatarの編集ページへのリンクに、"noopener"という値を定義したrel属性を追加しましょう。

target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。

この問題点を衝いた攻撃手法は、Tabnabbingと呼ばれます。タブナビング(Tabnabbing)とは | セキュリティ用語解説 | 日立ソリューションズの情報セキュリティブログ

Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。

対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけです。

具体的には、以下のような変更になりますすね。

- <a href="http://gravatar.com/emails" target="_blank">change</a>
+ <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>

app/views/layouts/_header.html.erb全体の変更内容は以下のようになります。

app/views/layouts/_header.html.erb
  <% provide(:title, "Edit user") %>
  <h1>Update your profile</h1>

  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user) do |f| %>
        ...略
      <% end %>

      <div class="gravatar_edit">
        <%= gravatar_for @user %>
-       <a href="http://gravatar.com/emails" target="_blank" >change</a>
+       <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
      </div>
    </div>
  </div>

2. リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。

まず、空のapp/views/layouts/_form.html.erbを作成するところから始まります。

>>> pwd
~/docker/rails_tutorial_test/sample_app
>>> touch app/views/layouts/_form.html.erb

作成したapp/views/users/_form.html.erbのソースを、次のように編集していきます。

app/views/users/_form.html.erb
<%= form_for(@user, url: yield(:path)) do |f| %>
  <%= render 'shared/error_messages', object: @user %>

  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

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

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

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

  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>

app/views/users/_form.html.erbの内容を埋め込むビューのソースコードに対しても編集を加えていきます。以下2つのファイルが編集対象となります。

  • app/views/users/new.html.erb
  • app/views/users/edit.html.erb
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<% provide(:path, signup_path) %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
app/views/users/edit.html.erb
<% provide(:title, 'Edit user') %>
<% provide(:button_text, 'Save changes') %>
<% provide(:path, user_path) %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>

追記

この部分の実装に手こずりました。Railsチュートリアル - Webフォームのコードを共通化する際に、フォームのPOST先をどうyieldするか - Qiitaにて、手こずった内容のみを切り出して記述しています。

編集の失敗

updateアクションの実装方針

以下の方針をとっていきます。

  • @user.update_attributesの戻り値に応じ、成功と失敗を判断し、分岐する
  • 失敗した場合の処理から実装していく
  • 失敗した場合の処理の実装が完了した後に、成功した場合の処理を実装する

この実装方針、createアクションの実装と似ていますね。

updateアクションの最初の実装

実装内容としては以下の通りです。

def update
  @user = User.find(params[:id])
  if @user.update_attributes(user_params)
    # TODO:更新に成功した場合の実装
  else
    render 'edit'
  end
end

app/controllers/users_controller.rbへの追加は以下のようになります。

app/controllers/users_controller.rb
  class UsersController < ApplicationController

    ...略

    def create
      @user = User.new(user_params)
      if @user.save
        log_in @user
        flash[:success] = "Welcome to the Sample App!"
        redirect_to @user
      else
        render 'new'
      end
    end

    def edit
      @user = User.find(params[:id])
    end

+   def update
+     @user = User.find(params[:id])
+     if @user.update_attributes(user_params)
+       # TODO:更新に成功した場合の実装
+     else
+       render 'edit'
+     end
+   end

    private  ...略

現状では、以下のような動作になります。

  • 更新に成功した場合は何もしない
  • 更新に失敗した場合、編集ページをレンダリングする

ソースコードを見ても、やはりcreateアクションの実装と似ていますね。

現時点でも、無効な情報を送信すると、何が無効であるかのエラーメッセージがレンダリングされる

createアクションの実装時点で、Userモデルのバリデーションとエラーメッセージのパーシャルを実装していました。そのため、updateアクションにおいても、無効な情報を送信すると何が無効であるかのエラーメッセージがレンダリングされます。例えば以下のスクリーンショットは、実際にエラーメッセージがレンダリングされている状況のものです。

スクリーンショット 2019-11-17 6.12.46.png

演習 - 編集の失敗

1. 編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。

前提

例えば、ID=2のユーザーに、以下のような情報を送信した場合を考えてみます。

  • Name…Hoge Hoge
  • Email…foobar@invalid
  • Password…(何も入力しない)
  • Password Confirmation…(何も入力しない)

上記の情報は、以下の理由で有効ではありません。

  • メールアドレスの形式がルールに合わない
  • パスワードを空にはできない
  • パスワードが短すぎる

rails serverの挙動

「Save changes」ボタンをクリックし、その後のrails serverの挙動を追いかけてみます。

Started PATCH "/users/2" ...略

PATCHリクエストが無事に発行されたようです。

Processing by UsersController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"+kYFeAEo8aAbvma48y+scR6qlRq4T+V+7UICoVuzt3BjQl1eaeMq4y0x0XBoDp2KYxsodGIn0Y6qAxcXW6Y0zw==", "user"=>{"name"=>"Hoge Hoge", "email"=>"foobar@invalid", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Save changes", "id"=>"2"}

リクエストのパラメーターの内容も問題ないようです。Railsの内部的な処理でいえば、paramsハッシュに入ってくる値ですね。

  User Load (2.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
   (0.2ms)  begin transaction
  User Exists (7.1ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "foobar@invalid"], ["id", 2], ["LIMIT", 1]]
   (0.2ms)  rollback transaction

rollback transactionというメッセージが出ています。(ユーザー情報が無効なため)変更内容のRDBへの反映がなされずにトランザクションが終了した、ということですね。

  Rendering users/edit.html.erb within layouts/application
  Rendered shared/_error_messages.html.erb (3.4ms)
  Rendered users/_form.html.erb (31.4ms)
  Rendered users/edit.html.erb within layouts/application (52.7ms)
  Rendered layouts/_rails_default.erb (292.4ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  User Load (4.9ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendered layouts/_header.html.erb (8.8ms)
  Rendered layouts/_footer.html.erb (1.4ms)
Completed 200 OK in 580ms (Views: 500.5ms | ActiveRecord: 15.1ms)

改めてeditビューがレンダリングされ、HTTPリクエストは 200 OK というコードが返ってきて終了しています。_error_messages.html.erbというパーシャル、すなわち、エラーメッセージもレンダリングされています。

編集失敗時のテスト

統合テストの作成

editアクションに対する統合テストは、以下のコマンドで作成します。

# rails generate integration_test users_edit
Running via Spring preloader in process 425
      invoke  test_unit
      create    test/integration/users_edit_test.rb

統合テストなのでintegration_test、Userリソースのeditアクションに対するテストなのでusers_editという名前、ということですね。

編集の失敗に対する簡単なテスト

test/integration/users_edit_test.rbには、はじめにUsersEditTestというクラスが生成されます。

test/integration/users_edit_test.rb
class UsersEditTest < ActionDispatch::IntegrationTest

end

既存のユーザー登録情報をfixtureから取得する

「既存のユーザー登録情報の内容を変更する」という動作なので、まずは「既存のユーザー登録情報」そのものが必要となります。そのため、まずはじめにsetupメソッドを定義し、既存のユーザー登録情報をfixtureから取得するようにします。

def setup
  @user = users(:rhakurei)
end

「編集失敗時のテスト」そのものを定義する

「編集失敗時のテスト」そのものを、「unsuccessful edit」という名前で定義します。

test "unsuccessful edit" do

end

editビューが描画されるかどうかを確認する

get edit_user_path(@user)
assert_template 'users/edit'

編集ページにGETメソッドでアクセスした上で、editビューが描画されるかどうかを確認しています。ここで渡すページのパスは、user_pathではなくedit_user_pathである必要があります。

正しくないユーザー情報でPATCHリクエストを発行する

patch user_path(@user), params: { user: { name: "",
                                          email: "foo@invalid",
                                          password:              "foo",
                                          password_confirmation: "bar" } }

@userを引数に取ったuser_path1に対し、paramsハッシュをパラメータとしてPATCHリクエストを発行しています。

paramsハッシュは、userハッシュを値として取っています。userハッシュは、あらゆる意味で正しくないユーザー情報を示すハッシュです。何が正しくないかを以下に示します。

  • nameキーに対する値が空文字列である
  • emailキーに対する文字列が、メールアドレスとして正しくない形式である
  • passwordキーに対する値とpassword_confirmationキーに対する値が一致しない
  • passwordキーに対する値もpassword_confirmationキーに対する値も、いずれも短すぎる(6文字を下回る)

正しくないのは以上4点です。

正しくないユーザー情報でPATCHリクエストを発行したことにより、Editビューが描画されることを確認する

assert_template 'users/edit'

リダイレクトなしでEditビューが描画されることを確認します。

全体のソースコード

現時点でテストが成功することを確認する

現時点では、「たった今実装した、Editページに対するテスト」「テストスイート全体」いずれに対してもテストは成功するはずです。早速やってみましょう。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 459
Started with run options --seed 46024

  1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.91981s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

# rails test
Running via Spring preloader in process 446
Started with run options --seed 39430

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

Finished in 3.24308s
30 tests, 77 assertions, 0 failures, 0 errors, 0 skips

無事テストが成功しましたね。

演習 - 編集失敗時のテスト

1. リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみてましょう。

ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。

assert_selectの使い方

ハイパーリンク以外にassert_selectを適用する場合、第1引数は以下のような形の文字列で与えることができます。

  • 存在を期待する要素名(例…assert_select "div"
  • 存在を期待する要素名#ID名(例…assert_select "div#profile"
  • 存在を期待する要素名.クラス名(例…assert_select "div.nav"

いずれの場合も、第2引数は要素の期待する内容となります(例…assert_select "div.nav", "foobar")。

今回のテスト内容に対応するソースコード

結果、今回のテスト内容に対応するソースコードは以下の通りになります。

assert_select "div.alert", "The form contains 4 errors."

test/integration/users_edit_test.rbのソースコード全体の変更内容は以下の通りになります。

test/integration/users_edit_test.rb
  require 'test_helper'

  class UsersEditTest < ActionDispatch::IntegrationTest
    def setup
      @user = users(:rhakurei)
    end

    test "unsuccessful edit" do
      ...略
      assert_template 'users/edit'
+     assert_select "div.alert", "The form contains 4 errors."
    end
  end

テストを実行してみる

test/integration/users_edit_test.rbに対してテストを実行してみます。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 472
Started with run options --seed 15929

  1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.89699s
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips

テストは無事成功しました。

テストを失敗させてみる

このテストは本当に正しい実装なのでしょうか。

先ほど、「今回与えるユーザー情報で、正しくない点は4点である」と言及しました。なので、「The form contains 4 errors.」となるわけですね。例えば、「4 errors」を「3 errors」にすれば、テストは失敗するはずです。

- assert_select "div.alert", "The form contains 4 errors."
+ assert_select "div.alert", "The form contains 3 errors."

上述の変更を保存した上で、再度テストを実行してみます。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 485
Started with run options --seed 61445

 FAIL["test_unsuccessful_edit", UsersEditTest, 1.6028200000000652]
 test_unsuccessful_edit#UsersEditTest (1.60s)
        <The form contains 3 errors.> expected but was
        <The form contains 4 errors.>..
        Expected 0 to be >= 1.
        test/integration/users_edit_test.rb:16:in `block in <class:UsersEditTest>'

  1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.60985s
1 tests, 3 assertions, 1 failures, 0 errors, 0 skips

無事(?)想定した形でテストが失敗しました。

TDDで編集を成功させる

そもそも「ユーザー情報の編集の成功」とはどういう状態であるか

PATCHリクエストのパラメータとして、有効なユーザー情報が与えられた場合を前提とします。このとき、以下の動作すべてが実現されることをもって「ユーザー情報の編集の成功」とします。

  • フラッシュメッセージが空でない
  • 編集された当該ユーザーのプロフィールページにリダイレクトされる
  • RDB内のユーザー情報が正しく更新される
  • パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集できる

ユーザー情報の編集の成功に対する振る舞いを実装するためのテスト

「編集成功時のテスト」そのものを定義する

「編集失敗時のテスト」そのものを、「successful edit」という名前で定義します。

test "successful edit" do

end

editビューが描画されるかどうかを確認する

get edit_user_path(@user)
assert_template 'users/edit'

この部分は編集失敗時と同じになります。

有効なユーザー名とメールアドレス

有効なユーザー名とメールアドレスを変数に格納します。

name = "Foo Bar"
email = "foo@bar.com"

後で「PATCHリクエストで与えたユーザー情報の内容とRDBに反映された内容は本当に同じか?」ということを確認する必要があるため、この時点で変数として定義しています。

正しいユーザー情報でPATCHリクエストを発行する

patch user_path(@user), params: {user: {  name: name,
                                          email: email,
                                          password: "",
                                          password_confirm: ""} }

@userを引数に取ったuser_path1に対し、paramsハッシュをパラメータとしてPATCHリクエストを発行しています。

先ほど変数に格納したユーザー名とメールアドレスの形式は正しいものであるため、現時点でも正しいユーザー情報となります。但し、「パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集する」動作については別途定義する必要があります。

フラッシュメッセージが空でないことを確認する

assert_not flash.empty?

フラッシュメッセージが空でなければテストが通ります。これは、「ユーザーの更新が正常に完了したならば、更新が完了した旨メッセージを出力する」という実装が確実になされるようにするためです。

PATCHリクエストがリダイレクトを返し、リダイレクト先が更新が行われたユーザーのプロフィールページであることを確認する

assert_redirected_to @user

PATCHリクエストがリダイレクトを返し、そのリダイレクト先が「更新が行われたユーザーのプロフィールページ(へのGETリクエスト)であればテストが通ります。実際にリダイレクト先に移動するわけではないため、follow_redirect!メソッドは実行しません。

ユーザーのプロフィールページを再読み込みし、変更内容がRDBに正しく反映されたことを確認する

@user.reload
assert_equal name, @user.name
assert_equal email, @user.email

まず、PATCHリクエストが返すリダイレクト先に移動しないでユーザーのプロフィールページを更新します。その上で、以下の事柄を確認します。

  • RDBに保存された当該ユーザー情報のnameカラムの値が、PATCHリクエストのパラメータとして与えたnameの属性値と一致すること
  • RDBに保存された当該ユーザー情報のemailカラムの値が、PATCHリクエストのパラメータとして与えたemailの属性値と一致すること

いずれも、一致すればテストが通ります。

テスト"successful edit"の全体像

ここまでの内容を踏まえて、テスト"successful edit"test/integration/users_edit_test.rbに実装していきます。

test/integration/users_edit_test.rb
  require 'test_helper'

  class UsersEditTest < ActionDispatch::IntegrationTest
    def setup
      @user = users(:rhakurei)
    end

    ...略


+   test "successful edit" do
+     get edit_user_path(@user)
+     assert_template 'users/edit'
+     name = "Foo Bar"
+     email = "foo@bar.com"
+     patch user_path(@user), params: {user: {  name: name,
+                                               email: email,
+                                               password: "",
+                                               password_confirm: ""} }
+     assert_not flash.empty?
+     assert_redirected_to @user
+     @user.reload
+     assert_equal name, @user.name
+     assert_equal email, @user.email
+   end
  end

現時点でテスト"successful edit"は成功しない

当然ながら、現時点でテスト"successful edit"は成功しません。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 537
Started with run options --seed 34712

 FAIL["test_successful_edit", UsersEditTest, 2.7275522999989334]
 test_successful_edit#UsersEditTest (2.73s)
        Expected true to be nil or false
        test/integration/users_edit_test.rb:28:in `block in <class:UsersEditTest>'

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

Finished in 2.79524s
2 tests, 5 assertions, 1 failures, 0 errors, 0 skips

最初に表面化する失敗は、「空であってはいけないフラッシュメッセージが空である」というものです。

test/integration/users_edit_test.rb:28とありますが、私の環境では以下の行でした。

test/integration/users_edit_test.rb(28行目)
assert_not flash.empty?

Usersコントローラーに、編集成功時の動作を実装する

app/controllers/users_controller.rbにおける、編集成功時の実装コードそのものは、新規ユーザー作成成功時の実装コードとほぼ同じです。

app/controllers/users_controller.rb
  class UsersController < ApplicationController

    ...略

    def create
      @user = User.new(user_params)
      if @user.save
        log_in @user
        flash[:success] = "Welcome to the Sample App!"
        redirect_to @user
      else
        render 'new'
      end
    end

    def edit
      @user = User.find(params[:id])
    end

    def update
      @user = User.find(params[:id])
      if @user.update_attributes(user_params)
+       flash[:success] = "Profile updated"
+       redirect_to @user
      else
        render 'edit'
      end
    end

    private  ...略
  end

実際の相違点は、flash[:success]の後のメッセージのみですね。

UsersController#editの実装が完了しても、まだテスト"successful edit"は成功しない

UsersController#editの実装は完了しましたが、この時点ではまだテスト"successful edit"は成功しません。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 550
Started with run options --seed 1354

 FAIL["test_successful_edit", UsersEditTest, 1.954527699999744]
 test_successful_edit#UsersEditTest (1.96s)
        Expected true to be nil or false
        test/integration/users_edit_test.rb:28:in `block in <class:UsersEditTest>'

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

Finished in 2.01404s
2 tests, 5 assertions, 1 failures, 0 errors, 0 skips

現段階でテストが通らない原因

テストが通らない原因は、テスト内容と実装の以下の差異が原因です。

  • Userモデルにおいては、「パスワードの長さは最短6文字以上」というバリデーションが実装されている
  • 一方、テスト内容においては、パスワード・パスワードの確認ともに空白文字列が与えられている

結果、現在のテスト内容では、Userモデルにおけるパスワードのバリデーションに抵触し、RDBの内容が変更されるには至りません。

Userモデルの定義を書き換え、パスワードが空のままでもRDBを更新できるようにする

「パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集する」という機能を実装し、テスト"successful edit"を成功させるためには、Userモデルにおけるパスワードのバリデーションの内容を変更する必要があります。具体的には、Userモデルのvalidatesallow_nil: trueというオプションを追加します。

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

    # 渡された文字列のハッシュ値を返す
    def self.digest(unencrypted_password)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
      BCrypt::Password.create(unencrypted_password, cost: cost)
    end

    ...略
  end

「パスワードが空のままでも更新できるようにする」とあります。しかしながら、空のパスワードそのものが有効なユーザー情報に反映されることはありません。なぜなら、パスワードのバリデーションについては、validatesによるもののほか、has_secure_passwordによるものが別途存在するためです。

但し、has_secure_passwordによるパスワードのバリデーションに抵触しても、パスワード以外の属性に対する変更はRDBに反映されます。こうして、「パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集する」という動作が実現するわけなのです。

今度こそテスト"successful edit"が成功する

ここまで実装を終えたところで、改めてusers_edit_test.rbに記述されたテストを実行してみましょう。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 578
Started with run options --seed 8165

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

Finished in 2.42118s
2 tests, 9 assertions, 0 failures, 0 errors, 0 skips

今度こそテストが成功しました!

演習 - TDDで編集を成功させる

1. 実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。

パスワード変更を伴わない場合

以下のような内容をユーザー編集フォームに入力します。ポイントは以下です。

  • 名前・メールアドレスともに有効である
  • メールアドレスはGrabatarに登録されているものとする
  • 名前は変更する
  • メールアドレスは変更しない
  • パスワード・パスワード確認はいずれも空欄とする

スクリーンショット 2019-11-17 20.29.48.png

「Save changes」ボタンをクリックしてみます。

スクリーンショット 2019-11-17 20.31.37.png

無事編集が成功しました。

rails serverに出力されたPATCHリクエストのログは、以下のようになっています。

Started PATCH "/users/2" for 172.17.0.1 at 2019-11-17 11:31:28 +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#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"j1DoIERut6dteOJnSRI/cjIx+rvGT5aMNdc0ZcbxLMYWVLAGLKVs5Fv3Va/SMw6JT4BH1RwnonxyliHTxuSveQ==", "user"=>{"name"=>"Foo bar baz", "email"=>"[Gravatarに登録されているメールアドレス]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Save changes", "id"=>"2"}
  User Load (5.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
   (0.2ms)  begin transaction
  User Exists (4.9ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "[Gravatarに登録されているメールアドレス]"], ["id", 2], ["LIMIT", 1]]
  SQL (13.1ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Foo bar baz"], ["updated_at", "2019-11-17 11:31:28.978381"], ["id", 2]]
   (13.1ms)  commit transaction
Redirected to http://localhost:8080/users/2
Completed 302 Found in 55ms (ActiveRecord: 36.5ms)

SQLのUPDATE文を見ると、name属性値のみ更新するUPDATE文が発行されていますね。

2. もしGravatarと紐付いていない適当なメールアドレス (foobar@example.comなど) に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみてましょう。

以下のような内容をユーザー編集フォームに入力します。foobar.foobar@example.com というメールアドレスは、Gravatarと紐付いていません。

スクリーンショット 2019-11-17 20.40.13.png

「Save changes」ボタンをクリックしてみます。

スクリーンショット 2019-11-17 20.42.30.png

プロフィール画像は、Gravatarのデフォルト画像になりました。


  1. どのアクションに関連付けられるかは、config/routes.rb内のresources :usersという記述により定義されています。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0