紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【7章】Ruby on Railsチュートリアル演習まとめ&解答例【7.4ユーザ登録】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 7章 7.4ユーザ登録の演習まとめ&解答例です。

個人の解答例なので、誤りがあればご指摘ください。

7.4.1 登録フォームの完成

本章での学び

現状では正しい値を入力しても、登録できない。 原因は、UsersController#createに対するテンプレートが存在しないため。

Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"R19xBQHx6R//YVtxxqec3W47tAAwFX7uF8SnWHMtwziEbsmDAMerAp7XlFJkUBou+339KEet8ddTuC7hHGq8jw==", "user"=>{"name"=>"yokoyan", "email"=>"yokoyan@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"}
   (0.2ms)  begin transaction
  User Exists (0.4ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "yokoyan@example.com"], ["LIMIT", 1]]
  SQL (3.1ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?)  [["name", "yokoyan"], ["email", "yokoyan@example.com"], ["created_at", 2017-04-30 21:48:03 UTC], ["updated_at", 2017-04-30 21:48:03 UTC], ["password_digest", "$2a$10$DIVwsH852f5C11ge4wnKqeOa4iWcXxzUuwejIeUeYxG1yQTZEpEBa"]]
   (13.0ms)  commit transaction
No template found for UsersController#create, rendering head :no_content
Completed 204 No Content in 128ms (ActiveRecord: 16.7ms)

ユーザー登録に成功した場合はページを描画せずに、ユーザーのプロフィールページにリダイレクトさせる。

  def create
    #@user = User.new(params[:user])   #これはRails3までの実装
    @user = User.new(user_params)
    if @user.save
      #保存の成功をここで扱う
      redirect_to @user
    else
      render 'new'
    end
  end

redirect_to @userは、redirect_to user_url @userと同じ意味になる。

user_urlは、routes.rbに、以下を記載すると自動的に生成される模様。(ここがわからない。。。)

  resources :users

正しく登録が完了すると、以下のURLに遷移するようになる。 (この場合はユーザIDが2のユーザ)

https://XXXXXX/users/2

image.png

つまり、user_urlは、ユーザ登録が完了したユーザの、ユーザ情報ページに遷移している。(名前付きルートuser_path(user)にGETでアクセスしたときと同じ画面に遷移している)

演習1

有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認してみましょう。

正しい情報が登録されていることを確認。

>> User.count
   (0.1ms)  SELECT COUNT(*) FROM "users"
=> 2
>> User.second
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "yokoyan", email: "yokoyan@example.com", created_at: "2017-04-30 21:48:03", updated_at: "2017-04-30 21:48:03", password_digest: "$2a$10$DIVwsH852f5C11ge4wnKqeOa4iWcXxzUuwejIeUeYxG...">

演習2

リスト 7.28を更新し、redirect_to user_url(@user)とredirect_to @userが同じ結果になることを確認してみましょう。

コントローラを以下の通り修正。

・・・略・・・
    if @user.save
      #保存の成功をここで扱う
      redirect_to user_url @user

jiroユーザを登録。

image.png

登録に成功。

image.png

コントローラを元に戻す。

・・・略・・・
    if @user.save
      #保存の成功をここで扱う
      redirect_to @user

7.4.2 flash

本章での学び

flash変数

ユーザ登録処理が成功した際に、画面にユーザ登録完了のメッセージを表示するために、flash変数を使用する。

      flash[:success] = "Welcome to the Sample App!"

全画面に適用するために、applicationのひな型であるhtml.erbにメッセージを組み込んでいる。alert-<% message_type %>の部分では、flash変数とcssを組み合わせることで、メッセージのレベルごとに適用するBootStrapのCSSを変えている。

  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <% flash.each do |message_type, message|%>
        <div class="alert alert-<% message_type %>"><%= message %></div>
      <% end %>

演習1

コンソールに移り、文字列内の式展開 (4.2.2) でシンボルを呼び出してみましょう。例えば"#{:success}“といったコードを実行すると、どんな値が返ってきますか? 確認してみてください。

?> "#{ :success }"                                                                                                                                                                
=> "success"

演習2

先ほどの演習で試した結果を参考に、リスト 7.30のflashはどのような結果になるか考えてみてください。

ハッシュのキーであるシンボル:successと、:dangerを使ってアクセスする。

>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> flash.each do |key, value|
?>   puts "#{key}"
>>   puts "#{value}"
>> end
success
It worked!
danger
It failed.
=> {:success=>"It worked!", :danger=>"It failed."}
>> 
?> "#{ flash[:success] }"                                                                                                                                                         
=> "It worked!"
>> "#{ flash[:danger] }"
=> "It failed."

7.4.3 実際のユーザー登録

本章での学び

データベースのリセット

$ rails db:migrate:resetで、データベースの内容をリセットできる。

yokoyan:~/workspace/sample_app (sign-up) $ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20170417215343 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0026s
== 20170417215343 CreateUsers: migrated (0.0028s) =============================

== 20170423125906 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0016s
== 20170423125906 AddIndexToUsersEmail: migrated (0.0017s) ====================

== 20170424041610 AddPasswordDigestToUsers: migrating =========================
-- add_column(:users, :password_digest, :string)
   -> 0.0011s
== 20170424041610 AddPasswordDigestToUsers: migrated (0.0012s) ================

確かにユーザ数が0になっていることを確認。

>> User.count
   (0.3ms)  SELECT COUNT(*) FROM "users"
=> 0

Rails Tutorialユーザで登録すると、緑色でメッセージが表示されることを確認。

image

画面のHTMLソースを確認すると、alert-successクラスが表示されている。

        <div class="alert alert-success">Welcome to the Sample App!</div>

演習1

Railsコンソールを使って、新しいユーザーが本当に作成されたのかもう一度チェックしてみましょう。結果は、リスト 7.32のようになるはずです。

find_byメソッドを使ってメールアドレスで検索する。

>> User.find_by(email: "example@railstutorial.org")
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "example@railstutorial.org"], ["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-03 00:16:34", updated_at: "2017-05-03 00:16:34", password_digest: "$2a$10$qH4zt2NtBpRAAP3JUiqha.uMvZOAN5oZYv5TNn04V/1...">

演習2

自分のメールアドレスでユーザー登録を試してみましょう。既にGravatarに登録している場合、適切な画像が表示されているか確認してみてください。

自分のメールアドレスで登録。 Gravatarで登録したユーザ画像が表示されることを確認。

image

7.4.4 成功時のテスト

本章での学び

assert_differenceアサーション

Railsテスティングガイドによると、

yieldされたブロックで評価された結果である式の戻り値における数値の違いをテストする。

とのこと。

assert_difference 'User.count', 1 doとすることで、assert_differenceの処理前後で、User.count(第一引数)の値が、1(第二引数の値)異なっているかをテストすることができる。

follow_redirect!メソッド

Railsテスティングガイドによると、

単一のリダイレクトレスポンスに従う。

とのこと。 チュートリアル内にも以下のように説明されている。

POSTリクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッド

つまり、follow_redirect!は、assert_difference内で、users_pathへのPOSTリクエスト(URL:/users、アクション:create)を送信した結果(レスポンス)を見て、controllerで指定しているリダイレクト先のuser_url @user(ユーザ登録完了後のユーザ画面)へ移動している。

  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
  def create
    ・・・略・・・
    if @user.save
      #保存の成功をここで扱う
      flash[:success] = "Welcome to the Sample App!"
      redirect_to user_url @user

そのため、follow_redirect!の後、特定のユーザを表示するページ(URL: /users/ユーザID)が表示されることを期待して、users/showテンプレートのテストを行っている。

  test "valid signup information" do
  ・・・略・・・
    follow_redirect!
    assert_template 'users/show'
  end

Userのルーティング、Userのshowアクション、show.html.erbビューがそれぞれ正常に動いているかをテストすることができる。

yokoyan:~/workspace/sample_app (sign-up) $ rails test
Running via Spring preloader in process 2559
Started with run options --seed 43581

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

Finished in 1.33079s
20 tests, 50 assertions, 0 failures, 0 errors, 0 skips

演習1

7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.34に最小限のテンプレートを用意しておいたので、参考にしてください (FILL_INの部分を適切なコードに置き換えると完成します)。ちなみに、テキストに対するテストは壊れやすいです。文量の少ないflashのキーであっても、それは同じです。筆者の場合、flashが空でないかをテストするだけの場合が多いです。

assert_notアサーション

Railsテスティングガイドによると、

testはfalseであると主張する。

つまり、問題文にあるように、flashの中身が空ではないことを主張できれば良い。

作成したテスト

  test "valid signup information" do
    ・・・略・・・
    assert_not flash.empty?
  end

実行結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (sign-up) $ rails test
Running via Spring preloader in process 2662
Started with run options --seed 206

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

Finished in 1.15631s
20 tests, 51 assertions, 0 failures, 0 errors, 0 skips

演習2

本文中でも指摘しましたが、flash用のHTML (リスト 7.31) は読みにくいです。より読みやすくしたリスト 7.35のコードに変更してみましょう。変更が終わったらテストスイートを実行し、正常に動作することを確認してください。なお、このコードでは、Railsのcontent_tagというヘルパーを使っています。

content_tagヘルパー

Railsドキュメントによると、

タグを動的に生成 HTMLとERBが混ざってしまう場合などに使用するとすっきり表現できる

とのこと。

作成したコード

このコードを、

      <div class="alert alert-<%= message_type %>"><%= message %></div>

こう変える。確かに読みやすい。

      <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>

テストがgreenになることを確認。 リファクタリングってこうしてやっていくんだなあ。

yokoyan:~/workspace/sample_app (sign-up) $ rails test
Running via Spring preloader in process 3788
Started with run options --seed 51425

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

Finished in 1.11761s
20 tests, 51 assertions, 0 failures, 0 errors, 0 skips

演習3

リスト 7.28のリダイレクトの行をコメントアウトすると、テストが失敗することを確認してみましょう。

対象箇所をコメントアウトする。

#redirect_to user_url @user

テストがredになることを確認。

yokoyan:~/workspace/sample_app (sign-up) $ rails test
Running via Spring preloader in process 3857
Started with run options --seed 14535

ERROR["test_valid_signup_information", UsersSignupTest, 0.9199776879977435]
 test_valid_signup_information#UsersSignupTest (0.92s)
RuntimeError:         RuntimeError: not a redirect! 204 No Content
            test/integration/users_signup_test.rb:36:in `block in <class:UsersSignupTest>'

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

Finished in 1.10693s
20 tests, 49 assertions, 0 failures, 1 errors, 0 skips

動作確認後はコメントアウトを戻す。

演習4

リスト 7.28で、@user.saveの部分をfalseに置き換えたとしましょう (バグを埋め込んでしまったと仮定してください)。このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか? テストコードを追って考えてみてください。

該当箇所を修正する。

  def create
    ・・・略・・・
    #if @user.save
    if false
      #保存の成功をここで扱う
      flash[:success] = "Welcome to the Sample App!"
      redirect_to user_url @user
    else
      render 'new'
    end
  end

テストがredになることを確認。

yokoyan:~/workspace/sample_app (sign-up) $ rails test
Running via Spring preloader in process 4076
Started with run options --seed 63186

 FAIL["test_valid_signup_information", UsersSignupTest, 1.072253059828654]
 test_valid_signup_information#UsersSignupTest (1.07s)
        "User.count" didn't change by 1.
        Expected: 1
          Actual: 0
        test/integration/users_signup_test.rb:30:in `block in <class:UsersSignupTest>'

 FAIL["test_invalid_signup_information", UsersSignupTest, 1.1198141309432685]
 test_invalid_signup_information#UsersSignupTest (1.12s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:17:in `block in <class:UsersSignupTest>'

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

Finished in 1.12681s
20 tests, 42 assertions, 2 failures, 0 errors, 0 skips

コンソールのエラーから読み取れる原因は、2点。

  • データベースへの保存処理がないため、エラーのクラスが表示できていないため
  • User.countが1増えていないため

あと、今回のテストのエラーログには出ていないが、以下もあると思う。

  • 期待値である/user/showにリダイレクトしていない(show.html.erbではなく、new.html.erbを表示してしまっている)

おわりに

ついにユーザ登録機能が動きました! ずいぶんWEBアプリケーションっぽくなってきました。

これからもPOSTやGETのHTTPリクエストと、 対応する名前付きルートとアクションを意識しながら、 何の処理が動くのかを考えながら作っていきます。

いやー、Rails楽しいなあ。