0
0

More than 3 years have passed since last update.

Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストのアクセス制御を、テスト駆動開発で実装する

Posted at

前提条件

Micropostsコントローラーは、「関連付けられたユーザーを通してマイクロポストにアクセスする」というユースケースが前提となります。そのため、createアクションやdestroyアクションを利用するユーザーは、ログイン済みでなければなりません。

マイクロポストのアクセス制御に対するテストを記述する

このようなアクセス制御に関する実装は、バグがあった場合の被害が大きくなります。開発者として安心を得るためにも、テスト駆動で実装していきましょう。

テストの実装箇所はtest/controllers/microposts_controller_test.rbとなります。

test/controllers/microposts_controller_test.rb
require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end
end

上記テストの要点は以下です。

  • 「未ログインのユーザーによって投げられた正しいリクエストに対し、マイクロポストの数が変化しないこと」を確認している
  • 「未ログインのユーザーが正しいリクエストを投げた場合、ログインページにリダイレクトされること」を確認している

最初にテストを記述した時点でのtest/controllers/microposts_controller_test.rbに対するテストの結果

この時点で、test/controllers/microposts_controller_test.rbに対するテストを行ってみましょう。

# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2236
Started with run options --seed 36069

ERROR["test_should_redirect_create_when_not_logged_in", MicropostsControllerTest, 1.9570221000176389]
 test_should_redirect_create_when_not_logged_in#MicropostsControllerTest (1.96s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'create' could not be found for MicropostsController
            test/controllers/microposts_controller_test.rb:11:in `block (2 levels) in <class:MicropostsControllerTest>'
            test/controllers/microposts_controller_test.rb:10:in `block in <class:MicropostsControllerTest>'

ERROR["test_should_redirect_destroy_when_not_logged_in", MicropostsControllerTest, 2.150527499994496]
 test_should_redirect_destroy_when_not_logged_in#MicropostsControllerTest (2.15s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'destroy' could not be found for MicropostsController
            test/controllers/microposts_controller_test.rb:18:in `block (2 levels) in <class:MicropostsControllerTest>'
            test/controllers/microposts_controller_test.rb:17:in `block in <class:MicropostsControllerTest>'

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

Finished in 2.16834s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

メッセージの内容からすると、「(Micropostsコントローラーに)createアクションもdestroyアクションも存在しない」という趣旨のエラーということになるでしょう。

余談 - deleteの引数にmicropostではなくmicropostsを使ってしまった場合

テスト「should redirect destroy when not logged in」のコードを、以下のように誤って記述してしまったら、テストの結果はどうなるでしょうか。

test "should redirect destroy when not logged in" do
  assert_no_difference 'Micropost.count' do
    delete microposts_path(@micropost) # <-- micropostではなくmicropostsになっている
  end
  assert_redirected_to login_url
end

結果は以下のようになります。

# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2223
Started with run options --seed 18530

...略

ERROR["test_should_redirect_destroy_when_not_logged_in", MicropostsControllerTest, 1.9526230999908876]
 test_should_redirect_destroy_when_not_logged_in#MicropostsControllerTest (1.95s)
ActionController::RoutingError:         ActionController::RoutingError: No route matches [DELETE] "/microposts.499495288"
            test/controllers/microposts_controller_test.rb:18:in `block (2 levels) in <class:MicropostsControllerTest>'
            test/controllers/microposts_controller_test.rb:17:in `block in <class:MicropostsControllerTest>'

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

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

以下のエラーメッセージが重要です。

ActionController::RoutingError: No route matches [DELETE]

テスト中にこのようなエラーメッセージが出現した場合は、「単数形と複数形の適用を間違えた」という可能性について検討してみるヒントといえます。

Micropostsコントローラーに、createアクションとdestroyアクションを実装する

ひとまずは、Micropostsコントローラーにcreateアクションとdestroyアクションを実装するところから始めましょう。

app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
+
+   def create
+   end
+
+   def destroy
+   end
  end

Micropostsコントローラーにcreateアクションとdestroyアクションを実装した時点でのテストの結果

# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2250
Started with run options --seed 15761

 FAIL["test_should_redirect_create_when_not_logged_in", MicropostsControllerTest, 2.1255913999921177]
 test_should_redirect_create_when_not_logged_in#MicropostsControllerTest (2.13s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        Response body: 
        test/controllers/microposts_controller_test.rb:13:in `block in <class:MicropostsControllerTest>'

 FAIL["test_should_redirect_destroy_when_not_logged_in", MicropostsControllerTest, 2.910650000005262]
 test_should_redirect_destroy_when_not_logged_in#MicropostsControllerTest (2.91s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        Response body: 
        test/controllers/microposts_controller_test.rb:20:in `block in <class:MicropostsControllerTest>'

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

Finished in 2.91299s
2 tests, 4 assertions, 2 failures, 0 errors, 0 skips

「リダイレクトされるべきところがリダイレクトされていない」という理由でテストが失敗しています。当該テストでリダイレクトされる条件は「ログインしていないとき」ということなので、「ログインしていなければ、ログインページにリダイレクトする」という処理の実装が必要となります。

…どこかで見たことがある実装ですね。

「ログインしていなければ、ログインページにリダイレクトする」という処理を、Micropostsコントローラーにも適用できるようにする

Usersコントローラーには、既に同様の処理を行うlogged_in_userメソッドが存在する

以下がUsersController#logged_in_userの実装となります。

UsersController#logged_in_user
def logged_in_user
  unless logged_in?
    store_location
    flash[:danger] = "Please log in."
    redirect_to login_url
  end
end

「ログインしていなければ、ログインページにリダイレクトする」という処理がバッチリ書かれていますね。これをMicropostsコントローラーで使えるようにしたいのです。

UsersController#logged_in_userメソッドの定義を、Applicationコントローラーに移す

というわけで、logged_in_userメソッドの実装箇所を、UsersControllerならびにMicropostsController双方のスーパークラスであるApplicationControllerに移していきます。

新たなapp/controllers/application_controller.rbの実装は以下のようになります。

app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    include SessionsHelper
+
+   private
+
+     # ユーザーのログインを確認する
+     def logged_in_user
+       unless logged_in?
+         store_location
+         flash[:danger] = "Please log in."
+         redirect_to login_url
+       end
+     end
  end

JavaやC#などの経験がある人であれば、「あれ?この場所でprivate呼び出して、UsersControllerMicropostsControllerlogged_in_userメソッド呼べるの?」という疑問を持つのではないでしょうか。その点については、後述「privateという言葉の意味について」という項目で解説しています。

privateという言葉の意味について

Rubyのクラス中で用いるprivateメソッドは、単に「以降で定義されるクラスメソッドを、レシーバ付きで呼び出せないようにする」という意味のメソッドです。逆に「レシーバさえつけなければ当該メソッドを呼び出すことが可能」ということをも意味します。

「レシーバをつけないで当該メソッドを呼び出せる場所」というのは、すなわち「当該クラス、もしくは当該クラスを継承したサブクラスのみ」を意味します。結果として、「privateメソッド以降で定義されるクラスメソッドは、当該クラス、もしくは当該クラスを継承したサブクラスでしか呼び出すことができない」という動作になるのです。

privateメソッド以降で定義されたメソッドも、当該クラスを継承したサブクラスで使用することが可能という事実は重要です。特にJavaやC#等から入ってきた人の場合、このあたりの部分での混乱に注意が必要かと思います。

MicropostsControllerにbeforeフィルターを追加する

app/controllers/microposts_controller.rbには、beforeフィルターとしてlogged_in_userを使う旨を記述する必要があります。対象となるアクションはcreateおよびdestroyです。

app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
+   before_action :logged_in_user, only: [:create, :destroy]

    def create
    end

    def destroy
    end
  end

MicropostsController内には、logged_in_userの定義を書く必要がない」というのがポイントです。スーパークラスであるApplicationControllerから継承してくるためです。

既存のUsersController#logged_in_userの定義を削除する

logged_in_userの実装箇所をActionControllerに移したので、既に存在するUsersController#logged_in_userの定義は削除する必要があります(続く演習でも、このテーマが取り上げられています)。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
    ...略

    private

      ...略

      # beforeアクション
-
-     # ログイン済みユーザーかどうか確認
-     def logged_in_user
-       unless logged_in?
-         store_location
-         flash[:danger] = "Please log in."
-         redirect_to login_url
-       end
-     end

      ...略
  end

test/controllers/microposts_controller_test.rb`に対するテストが成功するようになる

ここまでの実装が完了すれば、test/controllers/microposts_controller_test.rbに対するテストは成功するようになるはずです。

# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 2263
Started with run options --seed 2211

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

Finished in 1.17518s
2 tests, 4 assertions, 0 failures, 0 errors, 0 skips

無事テストが成功しましたね。これで「マイクロポストのアクセス制御を実装できた」といえます。

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