Railsのテストの書き方

前提知識

Railsのテストは、Ruby標準のテスト機能であるMinitestをWeb向けに拡張したもので、両方のリファレンスを活用します。

  • Railsテスティングガイド
    • Railsのテスト方法に関する網羅的なドキュメント
  • Minitest::Assersions
    • Ruby標準のアサーション機能であるMinitestのリファレンス。
    • 実行結果の妥当性をチェックする処理をアサーションと言う
    • RSpecはRuby標準・Rails標準とは別物であり、組み合わせて使うことはない
  • Rails標準のテスト準備
    • 多くのケースでデータベースのセットアップは必要
    • データ作成に手間をかけることは非推奨と思われる

事前準備

Railsのテストツールはminitestがバンドルされています。rails testコマンドがテストランナーのコマンドです。以下の指定方法をよく使いますが、さらに細かいオプションで単体ケースの実行なども可能です。

  • rails test
    • test/ディレクトリ以下のテストを一式実行
  • rails test test/some-directory
    • 特定ディレクトリ内のテストを一式実行。末尾にスラッシュがつくと適切に動作しない
  • rails test test/some-directory/some-test.rb
    • 特定ファイルのテストを実行

まず、rails testコマンドを実行してみて、DBMS関連のエラーで停止する場合にはテスト環境の事前準備が完了していない可能性が高いと言えます。

DB接続、スキーマ、fixtureを確認することで、テスト時セットアップ動作のトラブルに対処できます。

ミドルウェア設定

テストはRAILS_ENV=testで実行されます。RDBMSやRedisなどの接続設定は、config/database.ymlのtest用設定や、config/environments/test.rbなどで設定でき、depelopmentと異なる構成をとれます。

サーバーインスタンス構成はdevelopmentの実行環境を流用し、データベースのみ切り分ける設定でも問題はないでしょう。実行環境は自動選択され、たとえばrails console実行時はdevelopment環境で動作し、rails testはtest環境で動作する挙動になります。

DB側の事前セットアップはCREATE DATABASEとGRANTまでは完了しておく必要があります。また、あらかじめデーモン起動しておき、接続可能な状態が必要です。

また、アプリケーションでストアドプロシージャなどのDB機能を使用している場合、あらかじめRAILS_ENV=test用のDBにセットアップしておくことで、テストコードからも問題なく利用できます。 ridgepole でセットアップする場合は、以下のように-EオプションでDBを指定できます。

$ ridgepole -c config/database.yml -f config/Schemafile -a -E test

Rails minitestのDB準備

テスト実行時の準備動作として db/schema.rb が実行され、テーブルをセットアップします。

DBスキーマの管理ツールに、ridgepoleなど非標準の手段を利用している場合、schema.rbを最新の状態にしておく必要があります。

rails db:schema:dumpを実行するとschema.rbが更新されます。
リリース前にテストを実行する実情を考慮すると、このコマンドの実行環境はdevelopmentが妥当でしょう。

なお、テストコードからDBMSへの書き込み操作については、テストの最後にROLLBACKが走ります。基本的なテスト実行のためのクリーンアップ処理を追加する必要はありません。

fixture

テストにデータが必要かどうかは個別のテストケースに依存します。テストは冪等に実行すべきものであるため、データも個別のテストケース実装に近いレイヤのコードとして供給した方が良いでしょう。

Railsの場合、テーブル作成後のデータロードはfixtureを通じて行われます。
ロードされたfixtureオブジェクトはかなり高機能です。以下のようなUserモデルのfixtureの場合、User.find(1)とusers(:john)は同様のActiveRecordオブジェクトを返し、リレーションも機能します。

john:
  id: 1
  name: John
  email: [email protected]

デフォルトでは test/test_helper.rbのfixtures :allが設定されており、すべてのfixtureファイルをロードする挙動になります。テーブル名に対応するシンボルをカンマ区切りで列挙することで、特定のフィクスチャのみロードさせることも可能です。

fixturesはrails generateした際に空の定義が生成されることがあり、NULLカラムに満ちたデータを作成する挙動になります。 DBテーブル定義でNOT NULL制約を指定しているカラムがあると、空のfixturesロード時にエラーになります。

そのため、中途半端に空のfixtureを置いた状態ではテストを実行できないケースが多々起こります。初期段階でデータを利用しないテストを実行したい場合はtest_helper.rbのfixtures設定をコメントアウトすれば、データロードを回避できます。

セットアップコード

setupメソッドで事前処理、teardownメソッドで事後処理を書けます。 Webアプリケーションでは、状態に応じて挙動を変えるケースが多々あります。以下のようにclassを階層化することで、複数の状態をセットアップできます。

class SomeTest < ActiveSupport::TestCase
  class ValidCase < self
    setup do
      # セットアップA
    end
    
    test "OK with valid settings" do
    end
  end
  
  class InvalidCase < self
    setup do
      # セットアップB
    end
    
    test "NG with invalid settings" do
    end
  end
end

この例で、ValidCase内に書いたtestブロックは、セットアップAの状態でスタートし、InvalidCase内に書いたtestブロックは、セットアップBの状態でスタートします。

なお、テストケースでDBに加えられた変更はケース終了時にROLLBACKが走るため、手動でteardownで消す必要はありません。2番目に書いたテストケースが1番目の処理結果に基づくような書き方をするのも間違っています。

インテグレーション・テストの書き方

RailsはWeb特有のインテグレーション・テストの書き方をサポートしています。Webリクエストの中には複数の処理が含まれることが多く、一連のフロー中にエラーがないことを確認します。比較的少ないテストケースでカバレッジが上がりやすく、テスト効率は優れています。

そのためには、ActionDispatch::IntegrationTestを継承したテストを作成することで、urlにリクエストを送って挙動を確認するテストを実施できます。

書き方の例のコア部分は以下のようになります。

class SomeControllerTest < ActionDispatch::IntegrationTest
  test "should get users list" do
    get '/users/index'
    assert_response :success
    assert_select 'table#users'
  end
end
  • getメソッドにパスを指定してリクエストする
    • ほかにもpostやpatchメソッドなどHTTPリクエストに対応するメソッドがある
    • 第2引数にリクエストパラメータを指定できる
    • HTTPリクエストの詳細なカスタマイズについては、 ActionDispatch::Integration::RequestHelpersのAPIdoc 参照
  • assert_responseメソッドでHTTPステータスを判定する
    • リダイレクトを追跡するメソッドもある
    • assert_redirected_toを使うと次ステップのpathもチェックできる
  • assert_selectでViewに存在する特徴的なタグをチェックできる
    • CSSクエリセレクタと同様の書き方で指定
  • メール送信もテスト可能

結合テストはある状態(setupメソッドで再現)とパラメータを用いてリクエストした際の外形的な挙動を観察します。インスタンス変数など値のチェックは実施しません。
ログインセッションを再現する例などの応用については、 IntegrationtestクラスのAPIdoc に解説があります。

値のチェックは別途ユニットテストに書きますが、ユニットテストはよくあるテストの書き方とさして違わず、特筆すべき点はありません。

また、結合テストと似たようなテスト手法にシステムテストがあります。これはブラウザの操作をシミュレートするものです。ユーザー操作に最も近いテストではありますが、HTML構造に依存した書き方が必要となるため、やや低レベルのテストコードを書く必要があります。

Write系の確認方法

外形的なテストでWrite系の処理を確認する場合、以下のように1件増えていることのアサーションでチェックすると良いでしょう。
また、POSTリクエストもparamsオプションでモデルのパラメータを送信でき、異常な値のケースや攻撃のシミュレートもできます。

class SomeControllerTest < ActionDispatch::IntegrationTest
  test "should create new user" do
    assert_difference('User.count', 1) do
      post '/users', params: { name: "Julius Caesar" }
    end
    assert_response :redirect
  end
end

同様にdeleteも1件減っていることをチェックすると正常系をテストできます。

メールの確認

ActionMailerで送信するメール処理についても、以下のような方法でテスト可能です。

  • deliver_nowで送信しているメールについては、assert_difference('ActionMailer::Base.deliveries.size', +1)で送信件数の増加をカウントすることにより送信挙動を確認できる
  • deliver_laterで送信しているメールについては、assert_enqueued_emails(1)でキューをカウントすることで送信挙動を確認できる
  • 配信したメールは、ActionMailer::Base.deliveries.lastで取り出せる。subjectやtoの内容を確認する
  • deliver_laterの非同期処理は、perform_enqueued_jobsを実行した後であれば、同期送信のメールと同様に扱える

テストダブル・カバレッジ

スタブなどのテストダブルやカバレッジ計測については、Minitestで書ける範囲に制約があるため、gemを追加利用した方が無難でしょう。
mochaを利用する書き方を Rubyテストのスタブ実装・カバレッジ計測 で解説しています。

外部のAPIを呼び出しているような処理では、テストケースのsetupでスタブを実装するのが自然でしょう。

インスタンス変数のチェック

インテグーションテストでは変数のテストは非推奨であり、assert_selectでViewのレンダリング結果をテストするなど、より外形的な結果を確認すべきです。
ただし、リクエスト後にControllerオブジェクトを取得できるため、RubyのObject#instance_variable_getで以下のようにインスタンス変数を取得できます。

@controller.instance_variable_get('@data')
⁋ 2020/07/15↻ 2026/10/08