😘

Rails 4.2 のコントローラーのテストをRails 5.0の機能テストに書き直す

2021/10/22に公開

Ruby on Railsではコントローラーのテストの書き方が、Rails 5.0で変わりました。
Railsのバージョンを上げるときに、コントローラーのテストを書き直すかどうか判断に迷うことがあります。
概ね機械的に書き換えられる部分がもあります。それを明示して判断の助けになることを期待します。

一度、rails-controller-testing gemを入れて、Rails 5.0へのアップグレードを完了してから、コントローラーのテストを書き直す手順をオススメします。

この記事が対象とするテスティングフレームワークは、RSpecではなくMinitestです。

テストの書き方が変わった理由

Rails 4.2までは、コントローラーのテストはメソッドの単体テストでした。
入力として、コントローラーのメソッドに引数を渡し、戻り値が期待通りか確認します。

例えば、次のようなテストコードです。

class ClassDictionariesControllerTest < ActionController::TestCase
  test 'get the dictionary' do
    response = get :show, params: { target_id: targets(:one).name, format: :csv }
    assert_response :success
    assert_equal ['klass', 'http://example.com/klass'], response.body.split("\t")
  end
end

get :show, params: { target_id: targets(:one).name, format: :csv }が入力です。

  • showがアクションメソッド名
  • paramsが引数

です。

assert_response :success
assert_equal ['klass', 'http://example.com/klass'], response.body.split("\t")

で、レスポンスコードとレスポンスボディが期待通りか確認します。
ここまでは良い感じの単体テストです。

Railsではコントローラーとビューがインスタンス変数とテンプレートを介して値をやりとりします。
次の項目もテストしていました。

  • 宣言されたインスタンス変数
  • どのテンプレートがレンダリングされたか

例えば、次のようなテストコードです。

test 'should get index' do
   get targets_url, params: {}
   assert_not_nil assigns(:targets_grid)
end

targets_gridはインスタンス変数名を指しています。
assert_not_nil assigns(:targets_grid)はインスタンス変数@targets_gridに値が設定されていることを確認しています。

考えてみれば、このテストが少し実装に踏み込みすぎています。
例えば、リファクタリングのために、コントローラーとビューで使うインスタンス変数の名前を@targets_gridから、ただの@gridに変更すると、それでテストが壊れます。
あるいは、ビューからは@targets_gridを参照しなくなったあとも、このアサーションとコントローラーのインスタンス変数だけ残ってしまうかもしれません。

Rails 5.0では、コントローラーのテスト範囲をもう少し広げられるようにしました。
機能テストと呼ばれています。
入力は次の項目です。

  • HTTPメソッド
  • URL
  • パラメータ

出力はレスポンスです。
レスポンスには、

  • レスポンスコード
  • レスポンスボディ
  • レスポンスヘッダー

などが含まれています。
その値が期待通りか確認します。

例えば、次のようなテストコードです。

class ClassDictionariesControllerTest < ActionDispatch::IntegrationTest
    test 'get the dictionary' do
      get target_class_dictionary_url(targets(:one)), params: { format: :csv }
      assert_response :success
      assert_equal ['klass', 'http://example.com/klass'], @response.body.split("\t")
    end
end

見た目はあまりかわりません。意味は多少変わっています。

get target_class_dictionary_url(targets(:one)), params: { format: :csv }が入力です。

  • getがHTTPメソッド
  • target_class_dictionary_url(targets(:one))がURL
  • paramsが引数

です。
言われて見ればRailsにはルーティング機能があるので、HTTPメソッドとURLがあれば、コントローラーのメソッドは一意に定まります。

assert_response :success
assert_equal ['klass', 'http://example.com/klass'], @response.body.split("\t")

で、レスポンスコードとレスポンスボディが期待通りか確認します。
こちらもほとんど変わりません。
レスポンスが@rosponseに自動的に格納されるようになりました。

書き換え手順

テストが継承する親クラスを変更する

例えば

class TargetsControllerTest < ActionController::TestCase

class TargetsControllerTest < ActionDispatch::IntegrationTest

に置き換えます。

Deviseを使っている場合は、さらに

include Devise::Test::ControllerHelpers

include Devise::Test::IntegrationHelpers

に置き換えます。
この時点でテストを実行すると、書き換えていないテストケースがエラーになります。
エラーが起きてるテストケースを修正していきます。

メソッド呼び出し

エラーが起きるのはテストケース内の、コントローラーのアクションメソッド呼び出し部分です。
これを直していきます。

この作業は機械的に行えます。
すべてのエラーがなくなりテストが通れば、機能テストへの移行は完了します。

アクションメソッド名をURLに変更する

例えば、TargetControllerのindexメソッドを置き換えたときは、ルーティングをtargets#namesで絞り込んでヘルパーメソッドを探します。

bin/rails routes |grep targets#index

を実行します。

targets GET    /targets(.:format)        targets#index

targetsにurlをつけたtargets_urlをURLヘルパーとして使います。

grepで絞り込んだ時は、次のようにヘルパー名が表示されないことがあります。

~ bin/rails routes |grep targets#update
         PATCH  /targets/:id(.:format)  targets#update
         PUT    /targets/:id(.:format)  targets#update

この場合は bin/rails routes -c targets のようにコントローラーで絞り込んでルーティングを表示すると、ヘルパーメソッドを見つけやすいです。

~ bin/rails routes -c targets
       Prefix Verb   URI Pattern                 Controller#Action
names_targets GET    /targets/names(.:format)    targets#names
      targets GET    /targets(.:format)          targets#index
              POST   /targets(.:format)          targets#create
   new_target GET    /targets/new(.:format)      targets#new
  edit_target GET    /targets/:id/edit(.:format) targets#edit
       target GET    /targets/:id(.:format)      targets#show
              PATCH  /targets/:id(.:format)      targets#update
              PUT    /targets/:id(.:format)      targets#update
              DELETE /targets/:id(.:format)      targets#destroy
         root GET    /                           targets#index
~ bin/rails routes |grep targets#update

この場合はtargetにurlをつけたtarget_urlをURLヘルパーとして使います。
URLヘルパーには単数形と複数形があることに注意してください。

new
get :new, params: {}

get new_target_url, params: {}
create
post :create, params: { target: {...

post targets_url, params: { target: {...
index
get :index, params: {}

get targets_url, params: {}
show
get :show, params: { id: @target }

get target_url(@target), params: { }

idをパラメータで渡していた場合は、削除します。

edit
get :edit, params: { id: @target }

get edit_target_url(@target), params: {}

idをパラメータで渡していた場合は、削除します。

update
put :update, params: { id: @target, ...

put target_url(@target), params: { ...

idをパラメータで渡していた場合は、削除します。

destory
delete :destroy, params: { id: @target }

delete target_url(@target), params: {}

idをパラメータで渡していた場合は、削除します。

その他

変則的なルーティングも、ルーティングを絞り込めばヘルパーメソッドを見つけられます。

get :names, format: :json

get names_targets_url, params: { format: :json }

アサーションの置き換え

rails-controller-testing gem の使用をやめるには、アサーションで使っている

  • assigns
  • assert_template

を、置き換える必要があります。
これはテストケース毎に何を確認するか判断する必要があります。
機械的に置き換えることができません。

例えば、次のようなような置き換えです。

assert_not_nil assigns(:targets_grid)

assert_match @target.name, @response.body

前者はインスタンス変数が設定されていることを確認していました。
後者は設定されたインスタンス変数を使って、画面に表示されている値を確認しています。

一旦、機械的に機能テストへの移行してしまい、アサートの置き換えは段階的に進めていくのがよさそうです。

参考

Discussion