Rails4のFunctional Testには注意しよう


Railsはデフォルトではminitestでテストを書くようになっていて、テストは

  • Unit Test (test/models)
  • Functional Test (test/controllers)
  • Integration Test (test/integration)

に分類されています。

Unit TestとIntegration Testは良いんですが、端的に言えば、Functional Testは書かないのが良いです。コントローラに書いたコードをテストしたい場合は、Integration Testを書きましょう。

Rails 4までと、Rails 5以降では状況が変わってくるみたいです。
Rails 5をお使いの場合は、深く考えずにRailsがgしてくれたファイルにテストを書いて大丈夫です。

理由

Functional Testを書いていくと、実際にWebアプリとして動作させる場合との挙動の違いに悩むことがあります。

  1. paramsの扱いが本物のWebアプリケーションと違うことがある
  2. 一個のテストケースでは一個のコントローラのインスタンスが使い回されるので、テストの書き方とコントローラの実装によっては、本物のWebアプリケーションと挙動が変わる

もちろんこれらに注意しながらテストを書くこともできますが、特に1.にはまるとけっこう辛いので、雑に「Functional Testは書かない」という方針を採用するのが良いんじゃないかと思います。

paramsの扱い

JSONをPOSTするようなAPIのときにparamsが本物と違って困ることがあります。

test "post JSON" do
  post(:do_something, { id: 123 })
end

こんなテストでは、コントローラから見たときに、params[:id]123になっていて欲しいのですが、実際には"123"と文字列になってしまいます。Functional Testを使うと、paramsの値は全部文字列になり、ふつーにWebアプリとして動作させたときにはきちんとJSONの通りの値になります。

POSTされたJSONをそのままActiveRecordに引き渡すような場合には、ActiveRecordが上手いことやってくれるので問題にならないのですが、paramsから数字を読んできて計算しなくてはいけない場合とかに困ります。

注意が必要ですね。

インスタンス使い回しの問題

Railsで作られたWebアプリケーションでは、リクエスト一つ一つを処理するためにコントローラのインスタンスが新しく作られます。Functional Testでは一つのテストケースでは一つのコントローラのインスタンスが使い回されます。

行儀良くテストを書いていれば一つのテストケースの中で複数回アクションを呼ぶことはないでしょうし、行儀良くコントローラを実装していればアクション起動前のインスタンス変数の状態に依存するコードは書かないでしょうが、うっかりこの二つの条件に引っかかるテストとコントローラを書いてしまうと、テストが通るのに動作しないWebアプリケーションができてしまいます。

paramsの問題に比べると仮定が多くて、実際に問題のあるコード・テストになってしまうことは少ないとは思いますが、僕は実際に経験したことがあります。

  • 一つのテストケースの中でアクションを複数回呼ばない
  • アクションの中では依存するインスタンス変数に注意する

などのポイントに気を付けることもできますが、まーこういうのが問題になるのって気を付けていても見落とす場合ですよね。

まとめ

RailsのFunctional Testは注意するべき点がいくつかあるので、細心の注意を払ってテストを書かなくてはいけません。テストごときに細心の注意とか払いたくないし多分そのうち忘れて悲しいことになるので、雑な対処として「Functional Testは書かない」というのをお勧めします。

rspec-railsとかだとどうなってるんでしょうね。よく知りません。

Rails5ではどうなるか

5.0.0.rc1で試してみたところ、test/functionals以下にできるファイルはActionDispatch::IntegrationTestのサブクラスになっていました。Rails 4まではActionController::TestCaseのサブクラスだったので、変更されているようです。

ここで説明したつらさはすべてActionController::TestCaseに由来するものなので、Rails 5を使う場合は特に悩む必要がないようです。