Rails テスティングガイド

本ガイドは、Railsでテストを書く方法について解説します。

このガイドの内容:

  • Railsテスティング用語
  • アプリケーションに対する単体テスト、機能テスト、結合テスト、システムテスト(system test)の実施
  • その他の著名なテスティング方法とプラグインの紹介

目次

  1. Railsアプリケーションでテストを作成する理由
  2. テストを導入する
  3. テスト用データベース
  4. モデルのテスト
  5. コントローラの機能テスト
  6. 結合テスト
  7. システムテスト
  8. テストヘルパー
  9. ルーティングをテストする
  10. ビューをテストする
  11. ビューのパーシャルをテストする
  12. メーラーをテストする
  13. ジョブをテストする
  14. Action Cableをテストする
  15. 並列テスト
  16. eager loadingをテストする
  17. その他のテスティング関連リソース

1 Railsアプリケーションでテストを作成する理由

自動実行されるテストを書いておけば、ブラウザやコンソールでの手動テストよりも、コードが期待どおりに動作していることをより短時間で確認できます。テストが失敗すれば問題がすぐに明らかになり、開発プロセスの早い段階でバグを特定して修正できるようになります。テストの自動化によって、コードの信頼性が向上するだけでなく、変更に対する信頼性も向上します。

Railsでは、テストを簡単に記述できます。Railsに組み込まれているテストのサポートについては、次のセクションで詳しく説明します。

2 テストを導入する

Railsでは、新しいアプリケーションの作成時からテストが開発プロセスの中心になります。

2.1 テストをセットアップする

rails new アプリケーション名コマンドでRailsアプリケーションを作成すると、その場でtest/ディレクトリが作成されます。このディレクトリの内容を一覧表示すると、次のようになります。

$ ls -F test
application_system_test_case.rb  controllers/                     helpers/                         mailers/                         system/                          fixtures/                        integration/                     models/                          test_helper.rb

2.2 testディレクトリの構造

helpers/ディレクトリにはビューヘルパーのテストmailers/ディレクトリにはメーラーのテストmodels/ディレクトリにはモデル用のテストをそれぞれ保存します。

controllers/ディレクトリは、コントローラに関連するテストの置き場所で、ルーティングやビューに関連するテストもここに置きます。ここで行うテストではHTTPリクエストをシミュレートして、その結果に対してアサーションを行います。

integrationディレクトリは、コントローラ同士のやりとりのテストを置く場所として予約されています。

system/ディレクトリには、アプリケーションをブラウザから操作してテストするシステムテストが保存されます。システムテストを使うと、ユーザーが実際に体験する方法でアプリケーションをテストできるほか、JavaScriptのテストにも役立ちます。システムテストは、アプリケーションのブラウザ内テストを実行するCapybaraから継承した機能です。

フィクスチャはテストデータを編成する方法の1つであり、fixturesフォルダに置かれます。

フィクスチャは、テストで使うデータをモックアップする方法の一種です。フィクスチャを使うと、「実際の」データを使わずに済むようになります。これらはfixturesディレクトリに保存されます。詳しくはフィクスチャセクションで後述します。

ジョブテスト用のjobs/ディレクトリは、ジョブを最初に生成したときに作成されます。

test_helper.rbにはテスティングのデフォルト設定を記述します。

application_system_test_case.rbには、システムテストのデフォルトの設定を記述します。

2.3 test環境

デフォルトでは、すべてのRailsアプリケーションにはdevelopment環境、test環境、production環境の3つの環境があります。

それぞれの環境の設定はいずれも同様の方法で変更できます。テストの場合は、config/environments/test.rbにあるオプションを変更することでtest環境の設定を変更できます。

テストはRAILS_ENV=test環境で実行されます。これはRailsによって自動的に設定されます。

2.4 最初のテストを書く

ガイドのRails をはじめようでは、rails generate modelコマンドを紹介しました。ジェネレータでモデルを作成すると、test/ディレクトリの下にテストのスタブ(stub)が生成されます。

$ bin/rails generate model article title:string body:text
...
create  app/models/article.rb
create  test/models/article_test.rb
create  test/fixtures/articles.yml
...

test/models/article_test.rbに含まれるデフォルトのテストスタブの内容は以下のような感じになります。

require "test_helper"

class ArticleTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

Railsにおけるテスティングコードや用語の理解に役立てるため、このファイルの内容を上から順に見ていきましょう。

require "test_helper"

test_helper.rbファイルをrequireすると、テストで使うデフォルト設定としてtest_helper.rbが読み込まれます。このファイルに追加したメソッドは、require "test_helper"を追加したどのテストでも使えるようになります。

class ArticleTest < ActiveSupport::TestCase
  # ...
end

上のArticleTestクラスは、テストケース(test case)と呼ばれます。 このクラスはActiveSupport::TestCaseを継承しているので、ActiveSupport::TestCaseにあるすべてのメソッドをArticleTestで利用できます。利用できるメソッドのいくつかについては後述します

Minitest::TestActiveSupport::TestCaseのスーパークラス)を継承したクラス(テストケース)で定義される、test_で始まる個別のメソッドは、単にテスト(test)と呼ばれます(このtest_は小文字でなければなりません)。 従って、test_passwordおよびtest_valid_passwordと定義されたメソッド名は正式なテスト名となり、テストケースの実行時に自動的に実行されます。

Railsでは、テスト名を表す文字列とブロックを受け取るtestメソッドも利用できます。testメソッドを使うと、メソッド名の冒頭にtest_を追加した標準のMinitest::Unitテストが生成されます。これにより、メソッドの命名に気を遣わずに済み、次のような感じで書けます。

test "the truth" do
  assert true
end

上のコードは、以下のように書いた場合と同等に振る舞います。

def test_the_truth
  assert true
end

通常のメソッド定義を使ってもよいのですが、testマクロを適用することで、引用符で囲まれた読みやすいテスト名がテストメソッドの定義に変換されます。

テスト名からのメソッド名生成は、スペースをアンダースコア_に置き換えることによって行われます。Rubyではメソッド名にどんな文字列でも使えるので、生成されたメソッド名はRubyの正規な識別子である必要はなく、テスト名にパンクチュエーション(句読点)などの文字が含まれていても大丈夫です。メソッド名で普通でない文字を使おうとするとdefine_method呼び出しやsend呼び出しが必要になりますが、名前の付け方そのものには公式な制限はほとんどありません。

テストの以下の部分は、アサーション(assertion: 主張、表明)と呼ばれます。

assert true

アサーションとは、オブジェクトまたは式を評価して、期待された結果が得られるかどうかをチェックするコード行です。アサーションでは以下のようなチェックを行えます。

  • ある値が別の値と等しいかどうか
  • このオブジェクトはnilかどうか
  • コードのこの行で例外が発生するかどうか
  • ユーザーのパスワードが5文字より多いかどうか

1つのテストにアサーションを複数書くことも可能です。この場合、すべてのアサーションが成功しないとテストがパスしません。

2.5 最初の「失敗するテスト」

今度はテストが失敗した場合の結果を見てみましょう。そのためには、article_test.rbテストケースに、確実に失敗するテストを以下のように追加します。

この例では、特定の基準を満たさなければ記事は保存されないというアサーションが使われています。したがって、記事が正常に保存されると、テストは失敗し、テストの失敗が示されます。

require "test_helper"

class ArticleTest < ActiveSupport::TestCase
  test "should not save article without title" do
    article = Article.new
    assert_not article.save
  end
end

以下は、新しく追加したテストの実行結果です。

$ bin/rails test test/models/article_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 44656

# Running:

F

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:4]:
Expected true to be nil or false


bin/rails test test/models/article_test.rb:4



Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

  • 出力に含まれているFは失敗を表します。
  • Failureの下には、この失敗に対応するトレースと、失敗したテスト名が表示されています。
  • 次の数行はスタックトレースで、アサーションの実際の値と期待されていた値がその後に表示されています。
  • デフォルトのアサーションメッセージには、エラー箇所を特定するのに十分な情報が含まれています。

どのアサーションでも、以下のように失敗時のメッセージをオプションパラメータとして渡すことで、失敗時のメッセージをさらに読みやすくできます。

test "should not save article without title" do
  article = Article.new
  assert_not article.save, "Saved the article without a title"
end

テストを実行すると、以下のようにさらに読みやすいメッセージが表示されます。

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Saved the article without a title

このテストがパスするように、titleフィールドでモデルレベルのバリデーションを追加します。

class Article < ApplicationRecord
  validates :title, presence: true
end

このテストはパスするはずです。 テスト内の記事はtitleで初期化されていないため、モデルのバリデーションによって保存が阻止されます。テストを再度実行してみると、このことを確認できます。

$ bin/rails test test/models/article_test.rb:6
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 31252

# Running:

.

Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Fではなく小さな緑の点.が表示されているのは、テストが正常にパスしたことを表します。

ここでは、目的の機能で失敗するテストを最初に書いてから、機能を追加するコードを書く、という手順で進め、最後にテストが再度を実行するとパスすることを確認しました。ソフトウェア開発の世界では、このような「失敗するテストを最初に書いてからコードを修正する」アプローチをテスト駆動開発(Test-Driven Development : TDD)と呼んでいます。

2.6 エラーの表示内容

以下は、テスト中にエラーが発生すると、どのように表示されるかを確認するための、わざと誤りを含んだテストです。

test "should report error" do
  # このsome_undefined_variableはテストケースのどこにも定義されていない
  some_undefined_variable
  assert true
end

このテストを実行すると、通常よりも多くのメッセージがコンソールに表示されます。

$ bin/rails test test/models/article_test.rb
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 1808

# Running:

E

Error:
ArticleTest#test_should_report_error:
NameError: undefined local variable or method 'some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>
    test/models/article_test.rb:11:in 'block in <class:ArticleTest>'


bin/rails test test/models/article_test.rb:9

.

Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

今度はEが出力されます。これはエラーが発生したテストが1件あることを示しています。

テストスイートに含まれる各テストメソッドは、エラーまたはアサーション失敗が発生するとそこで実行を中止し、次のテストメソッドに進みます。Railsのテストメソッドは、デフォルトではすべてランダムな順序で実行されるようになっています(テストの実行順序はconfig.active_support.test_orderオプションで設定できます)。

テストが失敗すると、それに応じたバックトレースが出力されます。Railsはデフォルトでバックトレースをフィルタし、アプリケーションに関連するバックトレースのみを出力します。これによって、フレームワークから発生する不要な情報を排除して作成中のコードに集中できます。完全なバックトレースを参照しなければならない場合は、-b(または--backtrace)オプションを追加することでこの振る舞いを変更できます。

$ bin/rails test -b test/models/article_test.rb

このテストがエラーを発生せずにパスするには、assert_raisesを用いて以下のようにテストを変更します。

test "should report error" do
  # このsome_undefined_variableはテストケースのどこにも定義されていない
  assert_raises(NameError) do
    some_undefined_variable
  end
end

これでテストはパスするはずです。

2.7 minitestのアサーション

ここまでにいくつかのアサーションを紹介しました。アサーションは、テストの中心を担う重要な存在です。システムが計画通りに動作していることを実際に確認しているのはアサーションです。

以下は、Railsにデフォルトで組み込まれているテスティングライブラリであるminitestで使えるアサーションの抜粋です。 なお、[msg]パラメータは1個のオプション文字列メッセージであり、テストが失敗したときのメッセージをわかりやすくしたいときに指定できます。

2.7.1 assert( test, [msg] )
  • testはtrueであると主張する。
2.7.2 assert_not( test, [msg] )
  • testはfalseであると主張する。
2.7.3 assert_equal( expected, actual, [msg] )
  • expected == actualはtrueであると主張する。
2.7.4 assert_not_equal( expected, actual, [msg] )
  • expected != actualはtrueであると主張する。
2.7.5 assert_same( expected, actual, [msg] )
  • expected.equal?(actual)はtrueであると主張する。
2.7.6 assert_not_same( expected, actual, [msg] )
  • expected.equal?(actual)はfalseであると主張する。
2.7.7 assert_nil( obj, [msg] )
  • obj.nil?はtrueであると主張する。
2.7.8 assert_not_nil( obj, [msg] )
  • obj.nil?はfalseであると主張する。
2.7.9 assert_empty( obj, [msg] )
  • objempty?であると主張する。
2.7.10 assert_not_empty( obj, [msg] )
  • objempty?ではないと主張する。
2.7.11 assert_match( regexp, string, [msg] )
  • stringは正規表現(regexp)にマッチすると主張する。
2.7.12 assert_no_match( regexp, string, [msg] )
  • stringは正規表現(regexp)にマッチしないと主張する。
2.7.13 assert_includes( collection, obj, [msg] )
  • objcollectionに含まれると主張する。
2.7.14 assert_not_includes( collection, obj, [msg] )
  • objcollectionに含まれないと主張する。
2.7.15 assert_in_delta( expected, actual, [delta], [msg] )
  • expectedactualの個数の差はdelta以内であると主張する。
2.7.16 assert_not_in_delta( expected, actual, [delta], [msg] )
  • expectedactualの個数の差はdelta以内にはないと主張する。
2.7.17 assert_in_epsilon ( expected, actual, [epsilon], [msg] )
  • expectedactualの個数の差がepsilonより小さいと主張する。
2.7.18 assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )
  • expectedactualの数値にはepsilonより小さい相対誤差がないと主張する。
2.7.19 assert_throws( symbol, [msg] ) { block }
  • 与えられたブロックはシンボルをスローすると主張する。
2.7.20 assert_raises( exception1, exception2, ... ) { block }
  • 渡されたブロックから、渡された例外のいずれかが発生すると主張する。
2.7.21 assert_instance_of( class, obj, [msg] )
  • objclassのインスタンスであると主張する。
2.7.22 assert_not_instance_of( class, obj, [msg] )
  • objclassのインスタンスではないと主張する。
2.7.23 assert_kind_of( class, obj, [msg] )
  • objclassまたはそのサブクラスのインスタンスであると主張する。
2.7.24 assert_not_kind_of( class, obj, [msg] )
  • objclassまたはそのサブクラスのインスタンスではないと主張する。
2.7.25 assert_respond_to( obj, symbol, [msg] )
  • objsymbolに応答すると主張する。
2.7.26 assert_not_respond_to( obj, symbol, [msg] )
  • objsymbolに応答しないと主張する。
2.7.27 assert_operator( obj1, operator, [obj2], [msg] )
  • obj1.operator(obj2)はtrueであると主張する。
2.7.28 assert_not_operator( obj1, operator, [obj2], [msg] )
  • obj1.operator(obj2)はfalseであると主張する。
2.7.29 assert_predicate ( obj, predicate, [msg] )
  • obj.predicateはtrueであると主張する(例:assert_predicate str, :empty?)。
2.7.30 assert_not_predicate ( obj, predicate, [msg] )
  • obj.predicateはfalseであると主張する(例:assert_not_predicate str, :empty?)。
2.7.31 assert_error_reported(class) { block }
  • 指定のエラークラスがブロック内で報告されたことを主張する(例: assert_error_reported IOError { Rails.error.report(IOError.new("Oops")) })。
2.7.32 assert_no_error_reported { block }
  • ブロック内でエラーが報告されないことを主張する(例: assert_no_error_reported { perform_service }
2.7.33 flunk( [msg] )
  • 必ず失敗すると主張する。これはテストが未完成であることを示すのに便利。

これらはminitestがサポートするアサーションの一部に過ぎません。最新の完全なアサーションのリストについてはminitest APIドキュメント、特にMinitest::Assertionsを参照してください。

独自のアサーションを自作して利用することもできます。実際、Railsはまさにそれを行っているのです。Railsには開発を楽にしてくれる特殊なアサーションがいくつも追加されています。

アサーションの自作は高度なトピックなので、本ガイドでは扱いません。

2.8 Rails固有のアサーション

Railsはminitestフレームワークに以下のような独自のカスタムアサーションを追加しています。

2.8.1 assert_difference(expressions, difference = 1, message = nil)
  • yieldされたブロックで評価された結果である式の戻り値における数値の違いをテストする。
2.8.2 assert_no_difference(expressions, message = nil, &block)
  • 式を評価した結果の数値は、ブロックで渡されたものを呼び出す前と呼び出した後で違いがないと主張する。
2.8.3 assert_changes(expressions, message = nil, from:, to:, &block)
  • 式を評価した結果は、ブロックで渡されたものを呼び出す前と呼び出した後で違いがあると主張する。
2.8.4 assert_no_changes(expressions, message = nil, &block)
  • 式を評価した結果は、ブロックで渡されたものを呼び出す前と呼び出した後で違いがないと主張する。
2.8.5 assert_nothing_raised { block }
  • 渡されたブロックで例外が発生しないことを確認する。
2.8.6 assert_recognizes(expected_options, path, extras = {}, message = nil)
  • 渡されたパスのルーティングが正しく扱われ、(expected_optionsハッシュで渡された(解析オプションがパスと一致したことを主張する。 基本的にこのアサーションでは、Railsがexpected_optionsで渡されたルーティングを認識していると主張する。
2.8.7 assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
  • 渡されたオプションは、渡されたパスの生成に使えるものであると主張する(assert_recognizesと逆の動作)。 extrasパラメータは、クエリ文字列に追加リクエストがある場合にそのパラメータの名前と値をリクエストに渡すのに使われる。 messageパラメータにはアサーションが失敗した場合のカスタムエラーメッセージを渡せる。
2.8.8 assert_routing(expected_path, options, defaults = {}, extras = {}, message = nil)
  • pathoptionsが両方向に一致すると主張する。 つまり、pathoptionsを生成し、次にoptionspathを生成することを検証する。 これは基本的にassert_recognizesassert_generatesを1ステップに組み合わせたものである。 extrasハッシュを渡すと、アクションにクエリ文字列として提供されるオプションを指定できる。 messageパラメータを渡すと、失敗時に表示するカスタムエラーメッセージを指定できる。
2.8.9 assert_response(type, message = nil)
  • レスポンスが特定のステータスコードを持っていることを主張する。 :successを指定するとステータスコード200〜299を指定したことになり、同様に:redirectは300〜399、:missingは404、:errorは500〜599にそれぞれマッチする。 ステータスコードの数字や同等のシンボルを直接渡すこともできる。 詳しくはステータスコードの完全なリストおよびシンボルとステータスコードの対応リストを参照。
2.8.10 assert_redirected_to(options = {}, message = nil)
  • 渡されたリダイレクトオプションが、最後に実行されたアクションで呼び出されたリダイレクトのオプションと一致することを主張する。 assert_redirected_to root_pathなどの名前付きルートを渡すことも、assert_redirected_to @articleなどのActive Recordオブジェクトを渡すことも可能。
2.8.11 assert_queries_count(count = nil, include_schema: false, &block)
  • &blockがSQLクエリのint数値を生成することを主張する。
2.8.12 assert_no_queries(include_schema: false, &block)
  • &blockがSQLクエリを生成しないことを主張する。
2.8.13 assert_queries_match(pattern, count: nil, include_schema: false, &block)
  • &blockが指定のパターンにマッチするSQLクエリを生成することを主張する。
2.8.14 assert_no_queries_match(pattern, &block)
  • &blockが指定のパターンにマッチするSQLクエリを生成しないことを主張する。

これらのアサーションのいくつかについては次の章で説明します。

2.9 テストケースのアサーション

Minitest::Assertionsに定義されているassert_equalなどの基本的なアサーションは、あらゆるテストケース内で用いられているクラスで利用できます。実際には、以下から継承したクラスもRailsで利用できます。

各クラスにはMinitest::Assertionsが含まれているので、どのテストでも基本的なアサーションを利用できます。

minitestについて詳しくは、minitestのドキュメントを参照してください。

2.10 Railsのテストランナー

bin/rails testコマンドを使ってすべてのテストを一括実行できます。

個別のテストファイルを実行するには、bin/rails testコマンドにそのテストケースを含むファイル名を渡します。

$ bin/rails test test/models/article_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 1559

# Running:

..

Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.

2 runs, 3 assertions, 0 failures, 0 errors, 0 skips

上を実行すると、そのテストケースに含まれるメソッドがすべて実行されます。

あるテストケースの特定のテストメソッドだけを実行するには、-n(または--name)フラグでテストのメソッド名を指定します。

$ bin/rails test test/models/article_test.rb -n test_the_truth
Running 1 tests in a single process (parallelization threshold is 50)
Run options: -n test_the_truth --seed 43583

# Running:

.

Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

行番号を指定すると、特定の行だけをテストできます。

$ bin/rails test test/models/article_test.rb:6 # 特定のテストの特定行のみをテスト

行を範囲指定することで、特定の範囲のテストを実行することも可能です。

$ bin/rails test test/models/article_test.rb:6-20 # 6行目から20行目までテストを実行

ディレクトリを指定すると、そのディレクトリ内のすべてのテストを実行できます。

$ bin/rails test test/controllers # 指定ディレクトリのテストをすべて実行

テストランナーではこの他にも、「failing fast」やテスト終了時に必ずテストを出力するといったさまざまな機能が使えます。次を実行してテストランナーのドキュメントをチェックしてみましょう。

$ bin/rails test -h
Usage:
  bin/rails test [PATHS...]

Run tests except system tests

Examples:
    You can run a single test by appending a line number to a filename:

        bin/rails test test/models/user_test.rb:27

    You can run multiple tests with in a line range by appending the line range to a filename:

        bin/rails test test/models/user_test.rb:10-20

    You can run multiple files and directories at the same time:

        bin/rails test test/controllers test/integration/login_test.rb

    By default test failures and errors are reported inline during a run.

minitest options:
    -h, --help                       Display this help.
        --no-plugins                 Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS).
    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake
    -v, --verbose                    Verbose. Show progress processing files.
        --show-skips                 Show skipped at the end of run.
    -n, --name PATTERN               Filter run on /regexp/ or string.
        --exclude PATTERN            Exclude /regexp/ or string from run.
    -S, --skip CODES                 Skip reporting of certain types of results (eg E).

Known extensions: rails, pride
    -w, --warnings                   Run with Ruby warnings enabled
    -e, --environment ENV            Run tests in the ENV environment
    -b, --backtrace                  Show the complete backtrace
    -d, --defer-output               Output test failures and errors after the test run
    -f, --fail-fast                  Abort test run on first failure or error
    -c, --[no-]color                 Enable color in the output
        --profile [COUNT]            Enable profiling of tests and list the slowest test cases (default: 10)
    -p, --pride                      Pride. Show your testing pride!

3 テスト用データベース

Railsアプリケーションのほとんどは、データベースと密接なやりとりを行うので、テストでもデータベースが必要となります。このセクションでは、このテストデータベースをセットアップしてサンプルデータを入力する方法について解説します。

test環境セクションで説明したように、すべてのRailsアプリケーションにはdevelopment環境、test環境、production環境の3つの環境があります。それぞれの環境におけるデータベース設定はconfig/database.ymlで行います。

テスティング専用のデータベースを用いることで、他の環境から切り離された専用のテストデータをセットアップしてアクセスできるようになります。これにより、development環境やproduction環境のデータベースにあるデータを気にすることなく、確実にテストを実行できます。

3.1 テストデータベースのスキーマを管理する

テストを実行するには、テスト用データベースで最新のスキーマが必要です。

テストヘルパーは、テストデータベースに未完了のマイグレーションが残っていないかどうかをチェックします。マイグレーションがすべて終わっている場合、db/schema.rbdb/structure.sqlをテストデータベースに読み込みます。ペンディングされたマイグレーションがある場合、エラーが発生します。

このエラーが発生するということは、スキーマのマイグレーションが不完全であることを意味します。bin/rails db:migrate RAILS_ENV=testコマンドでテスト用データベースのマイグレーションを実行することで、スキーマが最新の状態になります。

既存のマイグレーションに変更が加えられていると、テストデータベースを再構築する必要があります。bin/rails test:dbを実行することで再構築できます。

3.2 フィクスチャ

よいテストを作成するにはよいテストデータを準備する必要があることを理解しておく必要があります。 Railsでは、フィクスチャを使ってテストデータの定義やカスタマイズを行えます。 網羅的なドキュメントについては、フィクスチャAPIドキュメントを参照してください。

3.2.1 フィクスチャとは何か

フィクスチャ(fixture)とは、一貫したデータセットを表す専門用語です。フィクスチャを使うことで、事前に定義したデータをテスト実行直前にtestデータベースに導入できます。フィクスチャはYAMLで記述するので、特定のデータベースに依存しません。1つのモデルにつき1つのフィクスチャファイルが作成されます。

フィクスチャは、テストで必要なあらゆるオブジェクトを作成できるように設計されているわけではありません。一般的なテストケースに適用可能なデフォルトデータのみを用いるようにすることで、フィクスチャを最も効果的に管理できます。

フィクスチャファイルはtest/fixtures/ディレクトリの下に置かれます。

3.2.2 YAML

YAML形式のフィクスチャは人間にとってとても読みやすく、サンプルデータを容易に記述できます。この形式のフィクスチャには.ymlというファイル拡張子が与えられます(users.ymlなど)。

YAMLフィクスチャファイルのサンプルを以下に示します。

# この行はYAMLのコメントである
david:
  name: David Heinemeier Hansson
  birthday: 1979-10-15
  profession: Systems development

steve:
  name: Steve Ross Kellock
  birthday: 1974-09-27
  profession: guy with keyboard
  • 各フィクスチャはフィクスチャ名とコロンで始まり、その下にインデントを追加して、コロン区切りのキーバリューペアのリストを配置します。
  • 通常、レコード間は空行で区切ります。
  • 行の先頭に#文字を置くことで、フィクスチャファイルにコメントを追加できます。

関連付けを使っている場合は、2つの異なるフィクスチャの間に参照ノードを定義できます。belongs_tohas_many関連付けの例を以下に示します。

# test/fixtures/categories.yml
web_frameworks:
  name: Web Frameworks
# test/fixtures/articles.yml
first:
  title: Welcome to Rails!
  category: web_frameworks
# test/fixtures/action_text/rich_texts.yml
first_content:
  record: first (Article)
  name: content
  body: <div>Hello, from <strong>a fixture</strong></div>

fixtures/articles.ymlにある記事firstcategoryキーの値がaboutになり、fixtures/action_text/rich_texts.ymlにあるfirst_contentエントリのrecordキーの値がfirst (Article)になっている点にもご注目ください。 これは、前者についてはActive Recordがfixtures/categories.ymlにあるカテゴリaboutを読み込むように、後者についてはAction Textがfixtures/articles.ymlにある記事firstを読み込むように指示しています。

関連付けを名前で相互参照するには、関連付けられたフィクスチャにあるid:属性を指定する代わりに、フィクスチャ名を使えます。Railsはテストの実行中に、自動的に主キーを割り当てて一貫性を保ちます。関連付けの場合の振る舞いについて詳しくは、フィクスチャAPIドキュメントを参照してください。

3.2.3 ファイル添付のフィクスチャ

Active Storageの添付ファイルレコードはActiveRecord::Baseインスタンスを継承しているので、Active Recordの他のモデルと同様にフィクスチャで設定できます。

thumbnail添付画像に関連付けられているArticleモデルのフィクスチャデータYAMLを考えてみましょう。

class Article < ApplicationRecord
  has_one_attached :thumbnail
end
# test/fixtures/articles.yml
first:
  title: An Article

image/pngエンコードされたファイルがtest/fixtures/files/first.pngに置かれているとすると、以下のYAMLフィクスチャエントリは、関連するActiveStorage::BlobActiveStorage::Attachmentレコードを生成します。

# test/fixtures/active_storage/blobs.yml
first_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename: "first.png" %>
# test/fixtures/active_storage/attachments.yml
first_thumbnail_attachment:
  name: thumbnail
  record: first (Article)
  blob: first_thumbnail_blob
3.2.4 ERBでフィクスチャにコードを埋め込む

ERBは、テンプレート内にRubyコードを埋め込むのに使われます。YAMLフィクスチャ形式のファイルは、Railsに読み込まれたときにERBによる事前処理が行われます。ERBを活用すれば、Rubyコードでサンプルデータを生成できます。たとえば、以下のコードを使えば1000人のユーザーを生成できます。

<% 1000.times do |n| %>
  user_<%= n %>:
    username: <%= "user#{n}" %>
    email: <%= "user#{n}@example.com" %>
<% end %>
3.2.5 フィクスチャの動作

Railsはデフォルトで、test/fixturesフォルダにあるすべてのフィクスチャを自動的に読み込みます。フィクスチャの読み込みは主に以下の3つの手順で行われます。

  1. フィクスチャに対応するテーブルに含まれている既存のデータをすべて削除する
  2. フィクスチャのデータをテーブルに読み込む
  3. フィクスチャに直接アクセスしたい場合はフィクスチャのデータをメソッドにダンプする

Railsでは、データベースから既存のデータベースを削除するために外部キーやチェック制約といった参照整合性(referential integrity)トリガーを無効にしようとします。テスト実行時のパーミッションエラーが発生して困っている場合は、test環境のデータベースユーザーがこれらのトリガーを無効にする特権を持っていることを確認してください(PostgreSQLの場合、すべてのトリガーを無効にできるのはsuperuserのみです。PostgreSQLのパーミッションについて詳しくはこちらの記事を参照してください)。

3.2.6 フィクスチャはActive Recordオブジェクトである

フィクスチャは、実はActive Recordのインスタンスです。前述の手順3にもあるように、フィクスチャはスコープがテストケースのローカルになっているメソッドを自動的に利用可能にしてくれるので、フィクスチャのオブジェクトに直接アクセスできます。以下に例を示します。

# davidという名前のフィクスチャに対応するUserオブジェクトを返す
users(:david)

# idで呼び出されたdavidのプロパティを返す
users(:david).id

# Userクラスで利用可能なメソッドにアクセスすることもできる
david = users(:david)
david.call(david.partner)

複数のフィクスチャを一括で取得するには、次のようにフィクスチャ名をリストで渡します。

# davidとsteveというフィクスチャを含む配列を返す
users(:david, :steve)

3.3 トランザクション

デフォルトのRailsは、テストをデータベーストランザクションに自動的にラップし、テスト完了後にロールバックします。これにより、テストは互いに独立し、データベースへの変更は1つのテスト内でのみ観測されるようになります。

class MyTest < ActiveSupport::TestCase
  test "newly created users are active by default" do
    # このテストは暗黙でデータベーストランザクションにラップされるので、
    # ここで作成したユーザーは他のテストからは見えない
    assert User.create.active?
  end
end

ただし、ActiveRecord::Base.current_transactionは引き続き意図したとおりに動作します。

class MyTest < ActiveSupport::TestCase
  test "current_transaction" do
    # テストを暗黙のトランザクションが囲んでいても、
    # current_transactionのアプリケーションレベルの意味論に影響しない
    assert User.current_transaction.blank?
  end
end

複数の書き込みデータベースが存在する場合、テストはそれらに対応する多数のトランザクションにラップされ、すべてがロールバックします。

3.3.1 テストのトランザクションをオプトアウトする

以下のようにすることで、個別のテストケースでトランザクションを無効にできます。

class MyTest < ActiveSupport::TestCase
  # このテストケースではテストをデータベーストランザクションで暗黙にラップしなくなる
  self.use_transactional_tests = false
end

4 モデルのテスト

モデルのテストは、アプリケーションのモデルとそれに関連するロジックをテストするときに使います。このロジックは、前述のセクションで説明したアサーションとフィクスチャでテストできます。

Railsのモデルのテストは、test/models/ディレクトリに保存します。 Railsには、モデルテストのスケルトンを作成するためのジェネレータも用意されています。

$ bin/rails generate test_unit:model article
create  test/models/article_test.rb

上のコマンドを実行すると、以下のスケルトンファイルが生成されます。

# article_test.rb
require "test_helper"

class ArticleTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

モデルのテストには、ActionMailer::TestCaseのような独自のスーパークラスはありません。代わりに、ActiveSupport::TestCaseを継承します。

5 コントローラの機能テスト

機能テスト(functional test)を作成するときは、コントローラのアクションがリクエストと期待される結果(レスポンス)をどのように処理するかをテストすることに重点を置きます。 コントローラの機能テストは、システムテストが適していない場合(APIのレスポンスを確認する場合など)に使われることがあります。

5.1 機能テストに含める項目

機能テストでは以下のような項目をテストします。

  • Webリクエストが成功したか
  • ユーザーが正しいページにリダイレクトされたか
  • ユーザー認証が成功したか
  • レスポンスのテンプレートに正しいオブジェクトが保存されたか

機能テストの振る舞いを最も手軽に見る方法は、scaffoldジェネレータでコントローラを生成することです。

$ bin/rails generate scaffold_controller article
...
create  app/controllers/articles_controller.rb
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

これによってArticleリソースのコントローラコードとテストが生成されるので、test/controllersにあるarticles_controller_test.rbファイルを開いてテストコードを見ることができます。

既にコントローラがあり、デフォルトの7つのアクションに対応するテストコードだけをscaffoldで生成したい場合は、以下のコマンドを実行します。

$ bin/rails generate test_unit:scaffold article
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

テストコードをscaffoldで生成すると、@article値が設定されて、テストファイル全体で使われるようになります。このarticleのインスタンスは、test/fixtures/articles.ymlファイルの:oneキーでネステッド属性を使います。テストを実行する前に、このフィクスチャファイルでこのキーと関連する値が設定されていることを確認してください。

articles_controller_test.rbファイルのtest_should_get_indexというテストについて見てみましょう。

# articles_controller_test.rb
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url
    assert_response :success
  end
end

test_should_get_indexというテストでは、indexという名前のアクションに対するリクエストをシミュレートします。同時に、有効なarticlesインスタンス変数がコントローラに割り当てられます。

getメソッドはWebリクエストを開始し、結果を@responseとして返します。このメソッドには以下の6つの引数を渡せます。

  • リクエストするコントローラアクションのURI。 これは文字列ヘルパーかルーティングヘルパーの形を取る(articles_urlなど)。
  • params: アクションに渡すリクエストパラメータのハッシュ(クエリの文字列パラメータまたはarticle変数など)。
  • headers: リクエストで渡されるヘッダーの設定に用いる。
  • env: リクエストの環境を必要に応じてカスタマイズするのに用いる。
  • xhr: リクエストがAjaxかどうかを指定する。 Ajaxの場合はtrueを設定。
  • as: 別のContent-Typeでエンコードされているリクエストに用いる。 デフォルトで:jsonをサポート。

上のキーワード引数はすべてオプションです。

例: 1件目のArticle:showアクションを呼び出し、HTTP_REFERERヘッダを設定する。

get article_url(Article.first), headers: { "HTTP_REFERER" => "http://example.com/home" }

別の例: 最後のArticle:updateアクションをAjaxリクエストとして呼び出し、paramstitleに新しいテキストを渡す。

patch article_url(Article.last), params: { article: { title: "updated" } }, xhr: true

さらに別の例: :createアクションを呼び出して新規記事を1件作成し、タイトルに使うテキストをJSONリクエストとしてparamsに渡す。

post articles_path, params: { article: { title: "Ahoy!" } }, as: :json

articles_controller_test.rbファイルにあるtest_should_create_articleテストを実行してみると、モデルレベルのバリデーションが新たに追加されることによってテストは失敗します。

articles_controller_test.rbファイルのtest_should_create_articleテストを変更して、テストがパスするようにしてみましょう。

test "should create article" do
  assert_difference("Article.count") do
    post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }
  end

  assert_redirected_to article_path(Article.last)
end

これで、すべてのテストを実行するとパスするようになったはずです。

BASIC認証セクションの手順に沿う場合は、すべてのテストをパスさせるためにテストのsetupブロックに以下を追加する必要があります。

post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }, headers: { Authorization: ActionController::HttpAuthentication::Basic.encode_credentials("dhh", "secret") }

5.2 機能テストで利用できるHTTPリクエストの種類

HTTPリクエストに精通していれば、getがHTTPリクエストの一種であることも既に理解していることでしょう。Railsの機能テストでは以下の6種類のHTTPリクエストがサポートされています。

  • get
  • post
  • patch
  • put
  • head
  • delete

これらはすべてメソッドとして利用できます。典型的なCRUDアプリケーションでよく使われるのはgetpostputdeleteです。

機能テストでは、そのリクエストがアクションで受け付けられるかどうかを検証せず、代わりに結果を重視します。アクションが受け付けられるかどうかをテストするのであれば、リクエストテストの方が目的に合っています。

5.3 XHR(Ajax)リクエストをテストする

AJAX(Asynchronous Javscript and XML)リクエストは、非同期HTTPリクエストを用いてサーバーからコンテンツを取得し、ページ全体を読み込まずにページの関連部分だけを更新する手法です。

getpostpatchputdeleteメソッドで、以下のようにxhr: trueを指定することでAjaxリクエストをテストできます。

test "AJAX request" do
  article = articles(:one)
  get article_url(article), xhr: true

  assert_equal "hello world", @response.body
  assert_equal "text/javascript", @response.media_type
end

5.4 その他のリクエストオブジェクトをテストする

リクエストが完了して処理されると、以下の3つのハッシュオブジェクトが利用可能になります。

  • cookies: 設定されているすべてのcookies。
  • flash: flash内のすべてのオブジェクト。
  • session: セッション変数に含まれるすべてのオブジェクト。

これらのハッシュは、通常のHashオブジェクトと同様に文字列をキーとして値を参照できます。たとえば以下のようにシンボル名による参照も可能です。

flash["gordon"]                # flash[:gordon]も可
session["shmession"]          # session[:shmession]も可
cookies["are_good_for_u"]     # cookies[:are_good_for_u]も可

5.5 利用可能なインスタンス変数

機能テストでは、リクエストが行われた後で以下の3つの専用インスタンス変数を使えるようになります。

  • @controller: リクエストを処理するコントローラ
  • @request: リクエストオブジェクト
  • @response: レスポンスオブジェクト
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url

    assert_equal "index", @controller.action_name
    assert_equal "application/x-www-form-urlencoded", @request.media_type
    assert_match "Articles", @response.body
  end
end

5.6 HTTPとヘッダーとCGI変数を設定する

HTTPヘッダーは、重要なメタデータを提供するためにHTTPリクエストとともに送信される情報です。

CGI変数は、Webサーバーとアプリケーション間で情報を交換するために使われる環境変数です。

HTTPヘッダーCGI変数は、以下のようにヘッダーとして渡すことでテストできるようになります。

# HTTPヘッダーを設定する
get articles_url, headers: { "Content-Type": "text/plain" } # カスタムヘッダーでリクエストをシミュレートする

# CGI変数を設定する
get articles_url, headers: { "HTTP_REFERER": "http://example.com/home" } # カスタム環境変数でリクエストをシミュレートする

5.7 flash通知をテストする

その他のリクエストオブジェクトをテストする セクションで説明したように、テストでアクセスできる3つのハッシュ オブジェクトの1つにflashがあります。本セクションでは、ユーザーが新しい記事の作成に成功するたびに、ブログアプリケーションでflashメッセージが表示されるかどうかをテストする方法について説明します。

まず、test_should_create_articleテストにアサーションを追加する必要があります。

test "should create article" do
  assert_difference("Article.count") do
    post articles_url, params: { article: { title: "Some title" } }
  end

  assert_redirected_to article_path(Article.last)
  assert_equal "Article was successfully created.", flash[:notice]
end

この時点でテストを実行すると、以下のように失敗するはずです。

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Running 1 tests in a single process (parallelization threshold is 50)
Run options: -n test_should_create_article --seed 32266

# Running:

F

Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.

  1) Failure:
ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:
--- expected
+++ actual
@@ -1 +1 @@
-"Article was successfully created."
+nil

1 runs, 4 assertions, 1 failures, 0 errors, 0 skips

今度はコントローラにflashメッセージを実装してみましょう。:createアクションは次のようになります。

def create
  @article = Article.new(article_params)

  if @article.save
    flash[:notice] = "Article was successfully created."
    redirect_to @article
  else
    render "new"
  end
end

テストを実行すると、今度はパスするはずです。

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Running 1 tests in a single process (parallelization threshold is 50)
Run options: -n test_should_create_article --seed 18981

# Running:

.

Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s.

1 runs, 4 assertions, 0 failures, 0 errors, 0 skips

scaffoldジェネレーターでコントローラを生成した場合、flashメッセージは既にcreateアクションに実装されています。

5.8 showupdatedestroyアクションをテストする

ここまでは、:indexアクションと:createアクションのテストについて説明しました。その他のアクションについてはどうでしょうか?

:showアクションのテストは次のように書けます。

test "should show article" do
  article = articles(:one)
  get article_url(article)
  assert_response :success
end

前述のフィクスチャで説明したように、articles()メソッドを使うと記事のフィクスチャにアクセスできることを思い出しましょう。

既存の記事の削除は次のようにテストします。

test "should delete article" do
  article = articles(:one)
  assert_difference("Article.count", -1) do
    delete article_url(article)
  end

  assert_redirected_to articles_path
end

既存の記事の更新テストも次のように書けます。

test "should update article" do
  article = articles(:one)

  patch article_url(article), params: { article: { title: "updated" } }

  assert_redirected_to article_path(article)
  # 更新されたデータをフェッチするために関連付けをリロードし、タイトルが更新されたというアサーションを行う
  article.reload
  assert_equal "updated", article.title
end

これら3つのテストは、どれも同じArticleフィクスチャデータへのアクセスを行っており、少々重複していますので、DRY(Don't Repeat Yourself)に書き換えましょう。これはActiveSupport::Callbacksが提供するsetupメソッドとteardownメソッドで行います。

書き換え後のテストは以下のようになるでしょう。

require "test_helper"

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # 各テストの実行前に呼ばれる
  setup do
    @article = articles(:one)
  end

  # 各テストの実行後に呼ばれる
  teardown do
    # コントローラがキャッシュを使っている場合、テスト後にリセットしておくとよい
    Rails.cache.clear
  end

  test "should show article" do
    # セットアップ時の@articleインスタンス変数を再利用
    get article_url(@article)
    assert_response :success
  end

  test "should destroy article" do
    assert_difference("Article.count", -1) do
      delete article_url(@article)
    end

    assert_redirected_to articles_path
  end

  test "should update article" do
    patch article_url(@article), params: { article: { title: "updated" } }

    assert_redirected_to article_path(@article)
    # 更新されたデータをフェッチするために関連付けをリロードし、タイトルが更新されたというアサーションを行う
    @article.reload
    assert_equal "updated", @article.title
  end
end

setupメソッドとteardownメソッドでも、Railsの他のコールバックと同様にブロックやlambdaを渡したり、メソッド名のシンボルを渡して呼び出したりできます。

6 結合テスト

結合テスト(integration test、統合テストとも)は、コントローラの機能テストをさらに一歩進めたもので、アプリケーションの複数の部分がどのようにやりとりするかをテストすることに重点を置いています。結合テストは、一般にアプリケーション内の重要なワークフローのテストに使われます。

Railsの結合テストは、test/integration/ディレクトリに保存されます。

Railsは、以下のような結合テスト用のスケルトンを作成するジェネレータを提供しています。

$ bin/rails generate integration_test user_flows
      exists  test/integration/
      create  test/integration/user_flows_test.rb

生成直後の結合テストは以下のような内容になっています。

require "test_helper"

class UserFlowsTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

結合テストはActionDispatch::IntegrationTestを継承しています。これにより、結合テスト内では標準のテストヘルパー以外にもさまざまなヘルパーを利用できます。

6.1 結合テストで利用できるヘルパー

システムテストでは、標準のテストヘルパー以外にもActionDispatch::IntegrationTestから継承されるいくつかのヘルパーを利用できます。その中から3つのカテゴリについて簡単にご紹介します。

結合テストランナーについてはActionDispatch::Integration::Runnerを参照してください。

リクエストの実行についてはActionDispatch::Integration::RequestHelpersにあるヘルパーを用いることにします。

ファイルをアップロードする必要がある場合は、ActionDispatch::TestProcess::FixtureFileを参照してください。

セッションを改変する必要がある場合や、結合テストのステートを変更する必要がある場合は、ActionDispatch::Integration::Sessionを参照してください。

6.2 結合テストを実装する

それではブログアプリケーションに結合テストを追加することにしましょう。最初に基本的なワークフローととして新しいブログ記事を1件作成し、すべて問題なく動作することを確認します。

まずは結合テストのスケルトンを生成します。

$ bin/rails generate integration_test blog_flow

上のコマンドを実行するとテストファイルのプレースホルダが作成され、次のように表示されるはずです。

      invoke  test_unit
      create    test/integration/blog_flow_test.rb

それではこのファイルを開いて最初のアサーションを書きましょう。

require "test_helper"

class BlogFlowTest < ActionDispatch::IntegrationTest
  test "can see the welcome page" do
    get "/"
    assert_select "h1", "Welcome#index"
  end
end

リクエストで生成されるHTMLをテストするassert_selectについてはこの後のビューをテストするで言及します。これはリクエストに対するレスポンスのテストに用いるもので、重要なHTML要素がコンテンツに存在するというアサーションを行います。

rootパスを表示すると、そのビューでwelcome/index.html.erbが表示されるはずなので、このアサーションはパスするはずです。

6.2.1 記事の結合テストを作成する

ブログに新しい記事を1件作成して、生成された記事が表示できていることをテストしましょう。

test "can create an article" do
  get "/articles/new"
  assert_response :success

  post "/articles",
    params: { article: { title: "can create", body: "article successfully." } }
  assert_response :redirect
  follow_redirect!
  assert_response :success
  assert_select "p", "Title:\n  can create"
end

理解のため、このテストを細かく分けてみましょう。

最初にArticlesコントローラの:newアクションを呼びます。このレスポンスは成功するはずです。

次にArticlesコントローラの:createアクションにPOSTリクエストを送信します。

post "/articles",
  params: { article: { title: "can create", body: "article successfully." } }
assert_response :redirect
follow_redirect!

その次の2行では、記事が1件作成されるときのリクエストのリダイレクトを扱います。

リダイレクト実行後に続いて別のリクエストを行う予定がある場合は、follow_redirect!を必ず呼び出してください。

最後は、レスポンスが成功して記事がページ上で読める状態になっているというアサーションです。

6.2.2 結合テストの利用法

ブログを表示して記事を1件作成するという、きわめて小規模なワークフローを無事テストできました。このテストにコメントを追加することも、記事の削除や編集のテストを行うこともできます。結合テストは、アプリケーションのあらゆるユースケースに伴うエクスペリエンスのテストに向いています。

7 システムテスト

システムテストはアプリケーションのユーザー操作のテストに使えます。テストは、実際のブラウザまたはヘッドレスブラウザに対して実行されます。システムテストではそのために背後でCapybaraを使います。

アプリケーションのtest/systemディレクトリは、Railsのシステムテストを作成するために使います。Railsではシステムテストのスケルトンを生成するジェネレータが提供されています。

$ bin/rails generate system_test users
      invoke test_unit
      create test/system/users_test.rb

生成直後のシステムテストは次のようになっています。

require "application_system_test_case"

class UsersTest < ApplicationSystemTestCase
  # test "visiting the index" do
  #   visit users_url
  #
  #   assert_selector "h1", text: "Users"
  # end
end

システムテストでは、デフォルトでSeleniumドライバと画面サイズ1400x1400のChromeブラウザを用いて実行されます。次のセクションで、デフォルト設定の変更方法について説明します。

7.1 デフォルト設定を変更する

Railsのシステムテストでは、デフォルト設定を非常にシンプルな方法で変更できます。すべての設定が抽象化されているので、テストの作成に集中できます。

新しいアプリケーションやscaffoldを生成すると、test/ディレクトリにapplication_system_test_case.rbファイルが作成されます。システムテストの設定はすべてここで行います。

デフォルト設定を変更したい場合は、システムテストのdriven_by項目を変更できます。たとえばドライバをSeleniumからCupriteに変更する場合は、まずcuprite gemをGemfileに追加してbundle installを実行し、次にapplication_system_test_case.rbを以下のように変更します。

require "test_helper"
require "capybara/cuprite"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :cuprite
end

このドライバ名はdriven_byの必須引数です。driven_byには、他に以下のオプション引数も渡せます。

  • :using: ブラウザを指定する(Seleniumでのみ有効)
  • :screen_size: スクリーンショットのサイズを変更する
  • :options: ドライバでサポートされるオプションを指定する
require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :firefox
end

ヘッドレスブラウザを使いたい場合は、以下のように:using引数にheadless_chromeまたはheadless_firefoxを渡すことで、ヘッドレスChromeやヘッドレスFirefoxを利用できます。

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome
end

DockerのヘッドレスChromeなどのリモートブラウザを使いたい場合は、以下のようにoptionsbrowserにリモートurlを追加する必要があります。

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  url = ENV.fetch("SELENIUM_REMOTE_URL", nil)
  options = if url
    { browser: :remote, url: url }
  else
    { browser: :chrome }
  end
  driven_by :selenium, using: :headless_chrome, options: options
end

これで、以下を実行すればリモートブラウザに接続されるはずです。

$ SELENIUM_REMOTE_URL=http://localhost:4444/wd/hub bin/rails test:system

テスト対象のアプリケーションがリモートでも動作している場合(Dockerコンテナなど)は、リモートサーバーの呼び出し方法に関する追加情報もCapybaraに渡す必要があります。

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  def setup
    Capybara.server_host = "0.0.0.0" # すべてのインターフェイスにバインドする
    Capybara.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}" if ENV["SELENIUM_REMOTE_URL"].present?
    super
  end
  # ...
end

これで、DockerコンテナとCIのどちらで動作していても、リモートブラウザとサーバに接続できるようになります。

Railsで提供されていないCapybara設定が必要な場合は、application_system_test_case.rbファイルに設定を追加できます。

追加設定についてはCapybaraのドキュメントを参照してください。

7.2 システムテストを実装する

それではアプリケーションにシステムテストを追加することにしましょう。システムテストで最初にindexページを表示し、新しいブログ記事を1件作成します。

scaffoldジェネレータを使った場合はシステムテストのスケルトンが自動で作成されています。scaffoldジェネレータを使わなかった場合はシステムテストのスケルトンを自分で作成しておきましょう。

$ bin/rails generate system_test articles

上のコマンドを実行するとテストファイルのプレースホルダが作成され、次のように表示されるはずです。

      invoke  test_unit
      create    test/system/articles_test.rb

それではこのファイルを開いて最初のアサーションを書きましょう。

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  test "viewing the index" do
    visit articles_path
    assert_selector "h1", text: "Articles"
  end
end

このテストは記事のindexページにh1要素が存在していればパスします。

システムテストを実行します。

bin/rails test:system

デフォルトではbin/rails testを実行してもシステムテストは実行されません。実際にシステムテストを実行するにはbin/rails test:systemを実行してください。

7.2.1 記事のシステムテストを作成する

それでは、新しい記事を作成するフローをテストしましょう。

test "should create Article" do
  visit articles_path

  click_on "New Article"

  fill_in "Title", with: "Creating an Article"
  fill_in "Body", with: "Created this article successfully!"

  click_on "Create Article"

  assert_text "Creating an Article"
end

最初の手順ではvisit articles_pathを呼び出し、記事のindexページの表示をテストします。

続いてclick_on "New Article"でindexページの「New Article」ボタンを検索します。するとブラウザが/articles/newにリダイレクトされます。

次に記事のタイトルと本文に指定のテキストを入力します。フィールドへの入力が終わったら「Create Article」をクリックして、データベースに新しい記事を作成するPOSTリクエストを/articles/createに送信します。

そして記事の元のindexページにリダイレクトされ、新しい記事のタイトルがその記事のindexページに表示されます。

7.2.2 さまざまな画面サイズでテストする

デスクトップのテストに加えてモバイルのサイズもテストしたい場合は、ActionDispatch::SystemTestCase から継承する別のクラスを作成し、テスト スイートで使用できます。この例では、mobile_system_test_case.rb というファイルが次の構成で /test ディレクトリに作成されます。

require "test_helper"

class MobileSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [375, 667]
end

この設定を使うには、test/systemディレクトリの下にMobileSystemTestCaseを継承したテストを作成します。 これで、さまざまな構成を使ってアプリをテストできるようになります。

require "mobile_system_test_case"

class PostsTest < MobileSystemTestCase
  test "visiting the index" do
    visit posts_url
    assert_selector "h1", text: "Posts"
  end
end
7.2.3 Capybaraのアサーション

以下は、Capybaraが提供している、システムテストで利用可能なアサーションの抜粋です。

7.2.3.1 assert_button(locator = nil, **options, &optional_filter_block)
  • 指定のテキスト、値、またはidを持つボタンがページに存在するかどうかをチェックする。
7.2.3.2 assert_current_path(string, **options)
  • ページが指定通りのパスに存在していることを主張する。
7.2.3.3 assert_field(locator = nil, **options, &optional_filter_block)
  • 指定のラベル、名前、またはidを持つフィールドがページのフォームに存在するかどうかをチェックする。
  • 指定のテキストまたはidを持つリンクがページに存在するかどうかをチェックする。
7.2.3.5 assert_selector(*args, &optional_filter_block)
  • セレクタで指定した要素(h1など)がページに存在することを主張する。
7.2.3.6 assert_table(locator = nil, **options, &optional_filter_block
  • 指定のidまたはキャプションを持つテーブルがページに存在するかどうかをチェックする。
7.2.3.7 assert_text(type, text, **options)
  • 指定のテキストコンテンツがページに存在することを主張する。
7.2.4 スクリーンショットヘルパー

ScreenshotHelperは、テスト中のスクリーンショットをキャプチャするよう設計されたヘルパーで、テストが失敗した時点のブラウザ画面を確認するときや、デバッグでスクリーンショットを確認するときに有用です。

take_screenshotメソッドおよびtake_failed_screenshotメソッドが提供されており、take_failed_screenshotはRails内部のafter_teardownに自動的に含まれます。

take_screenshotヘルパーメソッドはテストのどこにでも書くことができ、ブラウザのスクリーンショット撮影に使えます。

7.2.5 システムテストの利用法

システムテストの長所は、ユーザーによるやり取りをコントローラやモデルやビューを用いてテストできるという点で結合テストに似ていますが、本物のユーザーが操作しているかのようにテストを実際に実行できます。コメント入力や記事の削除、ドラフト記事の公開など、ユーザーがアプリケーションで行えるあらゆる操作をテストできます。

8 テストヘルパー

コードの重複を回避するために独自のテストヘルパーを追加できます。サインインヘルパーはその良い例です。

# test/test_helper.rb

module SignInHelper
  def sign_in_as(user)
    post sign_in_url(email: user.email, password: user.password)
  end
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end
require "test_helper"

class ProfileControllerTest < ActionDispatch::IntegrationTest
  test "should show profile" do
    # helper is now reusable from any controller test case
    sign_in_as users(:david)

    get profile_url
    assert_response :success
  end
end
8.1 ヘルパーを別ファイルに切り出す

ヘルパーが増えてtest_helper.rbが散らかってきたことに気づいたら、別のファイルに切り出せます。切り出したファイルの置き場所はtest/libtest/test_helpersがよいでしょう。

# test/test_helpers/multiple_assertions.rb
module MultipleAssertions
  def assert_multiple_of_forty_two(number)
    assert (number % 42 == 0), "expected #{number} to be a multiple of 42"
  end
end

これらのヘルパーは、必要に応じて明示的にrequireおよびincludeできます。

require "test_helper"
require "test_helpers/multiple_assertions"

class NumberTest < ActiveSupport::TestCase
  include MultipleAssertions

  test "420 is a multiple of 42" do
    assert_multiple_of_forty_two 420
  end
end

または、関連する親クラス内で直接includeすることもできます。

# test/test_helper.rb
require "test_helpers/sign_in_helper"

class ActionDispatch::IntegrationTest
  include SignInHelper
end
8.1.1 ヘルパーをeager requireする

ヘルパーをtest_helper.rb内でeagerにrequireできると、テストファイルが暗黙でヘルパーにアクセスできるので便利です。これは以下のようなglob記法(*)で実現できます。

# test/test_helper.rb
Dir[Rails.root.join("test", "test_helpers", "**", "*.rb")].each { |file| require file }

この方法のデメリットは、個別のテストで必要なファイルだけをrequireするよりも起動時間が長くなることです。

9 ルーティングをテストする

Railsアプリケーションの他のあらゆる部分と同様、ルーティングもテストできます。ルーティングのテストは、コントローラテストの一部としてtest/controllers/に配置します。

Railsで利用可能なルーティングアサーションについて詳しくは、ActionDispatch::Assertions::RoutingAssertionsのAPIドキュメントを参照してください。

10 ビューをテストする

アプリケーションのビューをテストする方法の1つは、リクエストに対するレスポンスをテストするために、あるページで重要なHTML要素とその内容がレスポンスに含まれているというアサーションを書くことです。ビューのテストも、ルーティングのテストと同様に、コントローラテストの一環としてtest/controllers/に配置します。

10.1 DOMによるHTML要素のアサーション

訳注: Rails 8のガイドでは、従来のassert_selectassert_select_*などによるアサーションが、assert_domassert_dom_*などに置き換えられました。

rails-dom-testing gemのassert_domassert_dom_equalなどのメソッドを使うと、レスポンスに含まれるHTML要素をシンプルかつ強力な構文でクエリできます。

assert_domは、一致する要素が見つかった場合にtrueを返すアサーションです。 たとえば、次のようにして、ページタイトルが「Welcome to the Rails Testing Guide」であることを確認できます。

assert_dom "title", "Welcome to the Rails Testing Guide"

より詳しくテストするために、assert_domブロックをネストすることも可能です。

以下の例の場合、外側のassert_domで選択されたすべての要素の完全なコレクションに対して、内側のassert_domがアサーションを実行します。

assert_dom "ul.navigation" do
  assert_dom "li.menu_item"
end

選択した要素のコレクションをイテレートし、各要素に対してassert_domを個別に呼び出すことも可能です。

たとえば、2つの順序付きリスト(ol)がレスポンスに含まれており、ネストされたリスト要素(li)がそれぞれのリストに4個ずつある場合、以下のテストは両方ともパスします。

assert_dom "ol" do |elements|
  elements.each do |element|
    assert_dom element, "li", 4
  end
end

assert_dom "ol" do
  assert_dom "li", 8
end

assert_dom_equal メソッドは、2つのHTML文字列を比較して、それらが等しいかどうかをチェックします。

assert_dom_equal '<a href="http://www.further-reading.com">Read more</a>',
  link_to("Read more", "http://www.further-reading.com")

さらに高度な利用法については、rails-dom-testing gemのドキュメントを参照してください。

rails-dom-testingと統合するため、ActionView::TestCaseを継承するテストは、レンダリングされたコンテンツをNokogiri::XML::Nodeのインスタンスとして返すdocument_root_elementメソッドを宣言します。

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article
  anchor = document_root_element.at("a")

  assert_equal article.name, anchor.text
  assert_equal article_url(article), anchor["href"]
end

アプリケーションがNokogiri(1.14.0以上)Minitest(5.18.0以上)に依存している場合、このdocument_root_elementはRubyのパターンマッチングをサポートします。

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article
  anchor = document_root_element.at("a")
  url = article_url(article)

  assert_pattern do
    anchor => { content: "Hello, world", attributes: [{ name: "href", value: url }] }
  end
end

システムテストで使うのと同じCapybaraベースのアサーションを使いたい場合は、ActionView::TestCaseを継承するベースクラスを以下のように定義することでdocument_root_elementpageメソッドに変換できます。

# test/view_partial_test_case.rb

require "test_helper"
require "capybara/minitest"

class ViewPartialTestCase < ActionView::TestCase
  include Capybara::Minitest::Assertions

  def page
    Capybara.string(rendered)
  end
end
# test/views/article_partial_test.rb

require "view_partial_test_case"

class ArticlePartialTest < ViewPartialTestCase
  test "renders a link to itself" do
    article = Article.create! title: "Hello, world"

    render "articles/article", article: article

    assert_link article.title, href: article_url(article)
  end
end

Capybaraに含まれるアサーションについて詳しくは、Capybaraベースのアサーションセクションを参照してください。

10.2 ビューのコンテンツを解析する

Action View 7.1以降の#renderedヘルパーメソッドは、ビューのパーシャルでレンダリングされたコンテンツを解析できるオブジェクトを返します。

#renderedメソッドが返すStringコンテンツをオブジェクトに変換するには、register_parserを呼び出してパーサーを定義します。.register_parser :rssを呼び出すと#rendered.rssヘルパーメソッドが定義されます。

たとえば、レンダリングした[RSSコンテンツ][RSS contents]を#rendered.rssで解析してオブジェクトにする場合は、以下のようにRSS::Parser.parse呼び出しを登録します。

register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }

test "renders RSS" do
  article = Article.create!(title: "Hello, world")

  render formats: :rss, partial: article

  assert_equal "Hello, world", rendered.rss.items.last.title
end

ActionView::TestCaseには、デフォルトで以下のパーサーが定義されています。

test "renders HTML" do
  article = Article.create!(title: "Hello, world")

  render partial: "articles/article", locals: { article: article }

  assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
end
test "renders JSON" do
  article = Article.create!(title: "Hello, world")

  render formats: :json, partial: "articles/article", locals: { article: article }

  assert_pattern { rendered.json => { title: "Hello, world" } }
end

10.3 ビューで使えるその他のアサーション

以下は、ビューのテストで利用できるその他ののアサーションです。

  • assert_dom_email: メールの本文に対するアサーションを行う。

  • assert_dom_encoded: エンコードされたHTMLに対するアサーションを行う。各要素のコンテンツをデコードしてから、すべての要素をブロックを呼び出す形で行われる。

  • css_select(selector)またはcss_select(element, selector): selector引数で選択されたすべての要素を1つの配列にしたものを返す。2番目の書式については、最初にelement引数がベース要素としてマッチし、続いてそのすべての子孫に対してselector引数のマッチを試みる。どちらの場合も、何も一致しなかった場合には空の配列を1つ返す。

assert_dom_emailの利用例を以下に示します。

assert_dom_email do
  assert_dom "small", "Please click the 'Unsubscribe' link if you want to opt-out."
end

11 ビューのパーシャルをテストする

パーシャルテンプレート(partial template: 部分テンプレートとも)は単にパーシャルとも呼ばれ、レンダリングプロセスを分割して管理しやすくできます。パーシャルを利用することで、コードの一部をテンプレートから別のファイルに切り出してさまざまな場所で再利用できるようになります。

ビューのテストでは、パーシャルが期待通りにコンテンツをレンダリングするかどうかをテストできます。ビューのパーシャルのテストはtest/views/に配置し、ActionView::TestCaseを継承します。

パーシャルをレンダリングするには、テンプレート内で行うのと同様にrenderメソッドを呼び出します。レンダリングしたコンテンツは、test環境やローカル環境の#renderedメソッドを通じて利用できます。

class ArticlePartialTest < ActionView::TestCase
  test "renders a link to itself" do
    article = Article.create! title: "Hello, world"

    render "articles/article", article: article

    assert_includes rendered, article.title
  end
end

ActionView::TestCaseを継承するテストでは、assert_domや、rails-dom-testing gemが提供するビューの追加アサーションも利用できるようになります。

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article

  assert_dom "a[href=?]", article_url(article), text: article.title
end

11.1 ビューヘルパーをテストする

ヘルパー自体は単なるシンプルなモジュールであり、ビューから利用するヘルパーメソッドをこの中に定義します。

ヘルパーのテストについては、ヘルパーメソッドの出力が期待どおりであるかどうかをチェックするだけで十分です。ヘルパー関連のテストはtest/helpersディレクトリに置かれます。

以下のようなヘルパーがあるとします。

module UsersHelper
  def link_to_user(user)
    link_to "#{user.first_name} #{user.last_name}", user
  end
end

このメソッドの出力は次のようにしてテストできます。

class UsersHelperTest < ActionView::TestCase
  test "should return the user's full name" do
    user = users(:david)

    assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user)
  end
end

さらに、このテストクラスはActionView::TestCaseを継承しているので、link_topluralizeなどのRailsヘルパーメソッドにもアクセスできます。

12 メーラーをテストする

メーラークラスを十分にテストするためには特殊なツールが若干必要になります。

12.1 メーラーのテストについて

Railsアプリケーションの他の部分と同様、メーラークラスについても期待どおり動作するかどうかをテストする必要があります。

メーラークラスをテストする目的は以下を確認することです。

  • メールが処理(作成および送信)されていること
  • メールの内容(subject、sender、bodyなど)が正しいこと
  • 適切なメールが適切なタイミングで送信されていること

メーラーのテストには単体テストと機能テストの2つの側面があります。

単体テストでは、完全に制御された入力を与えた結果の出力と、期待される既知の値(フィクスチャ)を比較します。

機能テストではメーラーによって作成される詳細部分についてのテストはほとんど行わず、コントローラとモデルがメーラーを正しく利用しているかどうかをテストするのが普通です。

メーラーのテストは、最終的に適切なメールが適切なタイミングで送信されたことを立証するために行います。

12.2 単体テスト

メーラーが期待どおりに動作しているかどうかをテストするために、事前に作成しておいた出力例と、メーラーの実際の出力を比較する形で単体テストを実行できます。

12.2.1 メーラーのフィクスチャ

メーラーの単体テストを行なうには、フィクスチャを利用してメーラーが最終的に出力すべき外見の例を与えます。 これらのフィクスチャはメールのサンプル出力であり、通常のフィクスチャのようなActive Recordデータではないので、通常のフィクスチャとは別の専用のサブディレクトリに保存します。

test/fixtures/ディレクトリの下のディレクトリ名は、メーラーの名前に対応させます。たとえばUserMailerという名前のメーラーであれば、test/fixtures/user_mailerというスネークケースのディレクトリ名にします。

メーラーを生成しても、メーラーのアクションに対応するスタブフィクスチャは生成されません。これらのファイルは上述の方法で手動作成する必要があります。

12.2.2 基本的なテストケース

inviteというアクションで知人に招待状を送信するUserMailerという名前のメーラーに対する単体テストを以下に示します。これは、inviteアクションをジェネレータで生成したときに作成される基本的なテストに手を加えたものです。

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # さらにアサーションを行うためにメールを作成して保存
    email = UserMailer.create_invite("me@example.com",
                                     "friend@example.com", Time.now)

    # メールを送信後キューに追加されるかどうかをテスト
    assert_emails 1 do
      email.deliver_now
    end

    # 送信されたメールの本文が期待どおりの内容であるかどうかをテスト
    assert_equal ["me@example.com"], email.from
    assert_equal ["friend@example.com"], email.to
    assert_equal "You have been invited by me@example.com", email.subject
    assert_equal read_fixture("invite").join, email.body.to_s
  end
end

このテストでは、メールを作成して、その結果返されたオブジェクトをemail変数に保存します。 次に、このメールが送信されたことを主張します(最初のアサーション)。 次のアサーションでは、メールの内容が期待どおりであることを主張します。なお、read_fixtureヘルパーはこのファイルからコンテンツを読み込むのに使います。

email.body.to_sは、HTMLまたはテキストで1回出現した場合にのみ存在するとみなされます。メーラーがHTMLとテキストの両方を提供している場合は、email.text_part.body.to_semail.html_part.body.to_sを用いてそれぞれの一部に対するフィクスチャをテストできます。

inviteフィクスチャは以下のような内容にしておきます。

friend@example.comさん、こんにちは。

招待状を送付いたします。

どうぞよろしく!
12.2.3 テスト時の配信方法を設定する

テスト時の送信モードは、config/environments/test.rbActionMailer::Base.delivery_method = :testという行で:testに設定されています。これにより、送信したメールが実際に配信されないようにできます。そうしないと、テスト中にユーザーにスパムメールを送りつけてしまうことになります。この設定で送信したメールは、実際にはActionMailer::Base.deliveriesという配列に追加されます。

このActionMailer::Base.deliveriesという配列は、ActionMailer::TestCaseActionDispatch::IntegrationTestでのテストを除き、自動的にはリセットされません。それら以外のテストで配列をクリアしたい場合は、ActionMailer::Base.deliveries.clearで手動リセットできます。

12.2.4 キューに登録されたメールをテストする

assert_enqueued_email_withアサーションを使うと、期待されるメーラーメソッドの引数やパラメータをすべて利用してメールがエンキュー(enqueue: キューへの登録)されたことを確認できます。これにより、deliver_laterメソッドでエンキューされたすべてのメールと照合できるようになります。

基本的なテストケースと同様に、メールを作成し、返されたオブジェクトをemail変数に保存します。引数やパラメータを渡すテスト例をいくつか紹介します。

以下の例は、メールが正しい引数でエンキューされたことを主張します。

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # メールを作成し、今後アサーションするために保存する
    email = UserMailer.create_invite("me@example.com", "friend@example.com")

    # 正しい引数でメールがエンキューされたことをテストする
    assert_enqueued_email_with UserMailer, :create_invite, args: ["me@example.com", "friend@example.com"] do
      email.deliver_later
    end
  end
end

以下の例は、引数のハッシュをargsとして渡すことで、メーラーメソッドの正しい名前付き引数でメールがエンキューされたことを主張します。

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # メールを作成し、今後アサーションするために保存する
    email = UserMailer.create_invite(from: "me@example.com", to: "friend@example.com")

    # 正しい名前付き引数でメールがエンキューされたことをテストする
    assert_enqueued_email_with UserMailer, :create_invite,
    args: [{ from: "me@example.com", to: "friend@example.com" }] do
      email.deliver_later
    end
  end
end

以下の例は、パラメータ化されたメーラーが正しいパラメータと引数でエンキューされたことを主張します。メーラーのパラメータはparamsとして、メーラーメソッドの引数はargsとして渡されます:

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # メールを作成し、今後アサーションするために保存する
    email = UserMailer.with(all: "good").create_invite("me@example.com", "friend@example.com")

    # パラメータと引数の正しいメーラーでメールがエンキューされたことをテストする
    assert_enqueued_email_with UserMailer, :create_invite,
    params: { all: "good" }, args: ["me@example.com", "friend@example.com"] do
      email.deliver_later
    end
  end
end

以下の例は、パラメータ化されたメーラーが正しいパラメータでエンキューされたかどうかをテストする別の方法です。

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # メールを作成し、今後アサーションするために保存する
    email = UserMailer.with(to: "friend@example.com").create_invite

    # パラメータの正しいメーラーでメールがエンキューされたことをテストする
    assert_enqueued_email_with UserMailer.with(to: "friend@example.com"), :create_invite do
      email.deliver_later
    end
  end
end

12.3 機能テストとシステムテスト

単体テストはメールの属性をテストできますが、機能テストとシステムテストを使えば、配信するメールがユーザー操作によって適切にトリガーされているかどうかをテストできます。

たとえば、友人を招待する操作によってメールが適切に送信されたかどうかを以下のようにチェックできます。

# 結合テスト
require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "invite friend" do
    # ActionMailer::Base.deliveriesの件数が変わるというアサーション
    assert_emails 1 do
      post invite_friend_url, params: { email: "friend@example.com" }
    end
  end
end
# システムテスト
require "test_helper"

class UsersTest < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome

  test "inviting a friend" do
    visit invite_users_url
    fill_in "Email", with: "friend@example.com"
    assert_emails 1 do
      click_on "Invite"
    end
  end
end

assert_emailsメソッドは特定の配信方法に紐付けられていないので、deliver_nowメソッドとdeliver_laterメソッドのどちらでメールを配信する場合にも利用できます。メールがキューに登録されたことを明示的なアサーションにしたい場合は、assert_enqueued_email_withメソッド(上述の例を参照)か、assert_enqueued_emailsメソッドを利用できます。詳しくはActionMailer::TestHelper APIドキュメントを参照してください。

13 ジョブをテストする

複数のジョブを分離してテストする(ジョブの振る舞いに注目する)ことも、コンテキスト内でテストする(呼び出し元のコードの振る舞いに注目する)ことも可能です。

13.1 ジョブを分離してテストする

ジョブを生成すると、ジョブに関連するテストもtest/jobs/ディレクトリの下に生成されます。

以下は請求(billing)ジョブの例です。

require "test_helper"

class BillingJobTest < ActiveJob::TestCase
  test "account is charged" do
    perform_enqueued_jobs do
      BillingJob.perform_later(account, product)
    end
    assert account.reload.charged_for?(product)
  end
end

テスト用のデフォルトキューアダプタは、perform_enqueued_jobsが呼び出されるまでジョブを実行しません。さらに、テスト同士が干渉しないようにするため、個別のテストを実行する前にすべてのジョブをクリアします。

このテストでは、perform_nowではなく、perform_enqueued_jobsperform_laterを使っています。こうすることで、リトライが設定されている場合には失敗したリトライが(再度エンキューされて無視されずに)テストでキャッチされるようになります。

13.2 ジョブをコンテキストでテストする

コントローラなどで、呼び出しのたびにジョブが正しくエンキューされているかどうかをテストするのはよい方法です。ActiveJob::TestHelperモジュールは、そのために役立つassert_enqueued_withなどのメソッドを提供しています。

以下はAccountモデルのメソッドをテストする例です。

require "test_helper"

class AccountTest < ActiveSupport::TestCase
  include ActiveJob::TestHelper

  test "#charge_for enqueues billing job" do
    assert_enqueued_with(job: BillingJob) do
      account.charge_for(product)
    end

    assert_not account.reload.charged_for?(product)

    perform_enqueued_jobs

    assert account.reload.charged_for?(product)
  end
end

13.3 例外が発生することをテストする

特にリトライが設定されている場合は、特定のケースでジョブが例外を発生することをテストするのが難しくなることもあります。

perform_enqueued_jobsヘルパーは、ジョブが例外を発生するとテストが失敗するので、例外の発生時にテストを成功させるには、以下のようにジョブのperformメソッドを直接呼び出す必要があります。

require "test_helper"

class BillingJobTest < ActiveJob::TestCase
  test "does not charge accounts with insufficient funds" do
    assert_raises(InsufficientFundsError) do
      BillingJob.new(empty_account, product).perform
    end
    assert_not account.reload.charged_for?(product)
  end
end

この方法はフレームワークの一部(引数のシリアライズなど)を回避するため、一般には推奨されていません。

14 Action Cableをテストする

Action Cableはアプリケーション内部の異なるレベルで用いられるため、チャネル、コネクションのクラス自身、および他のエンティティがいずれも正しいメッセージをブロードキャストすることをテストする必要があります。

14.1 コネクションのテストケース

デフォルトでは、Action Cableを用いる新しいRailsアプリを生成すると、基本のコネクションクラス(ApplicationCable::Connection)のテストもtest/channels/application_cable/ディレクトリの下に生成されます。

コネクションテストの目的は、コネクションのidが正しく代入されているか、正しくないコネクションリクエストを却下できるかどうかをチェックすることです。以下はテスト例です。

class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
  test "connects with params" do
    # `connect`メソッドを呼ぶことでコネクションのオープンをシミュレートする
    connect params: { user_id: 42 }

    # テストでは`connection`でConnectionオブジェクトにアクセスできる
    assert_equal connection.user_id, "42"
  end

  test "rejects connection without params" do
    # コネクションが却下されたことを
    # `assert_reject_connection`マッチャーで検証する
    assert_reject_connection { connect }
  end
end

リクエストのcookieも、結合テストの場合と同様の方法で指定できます。

test "connects with cookies" do
  cookies.signed[:user_id] = "42"

  connect

  assert_equal connection.user_id, "42"
end

詳しくはActionCable::Connection::TestCase APIドキュメントを参照してください。

14.2 チャネルのテストケース

デフォルトでは、チャネルをジェネレータで生成するときにtest/channels/ディレクトリの下に関連するテストも生成されます。以下はチャット用チャネルのテスト例です。

require "test_helper"

class ChatChannelTest < ActionCable::Channel::TestCase
  test "subscribes and stream for room" do
    # `subscribe`を呼ぶことでサブスクリプション作成をシミュレートする
    subscribe room: "15"

    # テストでは`subscription`でChannelオブジェクトにアクセスできる
    assert subscription.confirmed?
    assert_has_stream "chat_15"
  end
end

このテストはかなりシンプルであり、チャネルが特定のストリームへのコネクションをサブスクライブするアサーションしかありません。

背後のコネクションidも指定できます。以下はWeb通知チャネルのテスト例です。

require "test_helper"

class WebNotificationsChannelTest < ActionCable::Channel::TestCase
  test "subscribes and stream for user" do
    stub_connection current_user: users(:john)

    subscribe

    assert_has_stream_for users(:john)
  end
end

詳しくはActionCable::Channel::TestCase APIドキュメントを参照してください。

14.3 他のコンポーネント内でのカスタムアサーションとブロードキャストテスト

Action Cableには、冗長なテストを削減するのに使えるカスタムアサーションが多数用意されています。利用できる全アサーションのリストについては、ActionCable::TestHelper APIドキュメントを参照してください。

正しいメッセージがブロードキャストされたことを(コントローラ内部などの)他のコンポーネント内で確認するのはよい方法です。Action Cableが提供するカスタムアサーションの有用さは、まさにここで発揮されます。たとえばモデル内では以下のように書けます。

require "test_helper"

class ProductTest < ActionCable::TestCase
  test "broadcast status after charge" do
    assert_broadcast_on("products:#{product.id}", type: "charged") do
      product.charge(account)
    end
  end
end

Channel.broadcast_toによるブロードキャストをテストしたい場合は、Channel.broadcasting_forで背後のストリーム名を生成します。

# app/jobs/chat_relay_job.rb
class ChatRelayJob < ApplicationJob
  def perform(room, message)
    ChatChannel.broadcast_to room, text: message
  end
end
# test/jobs/chat_relay_job_test.rb
require "test_helper"

class ChatRelayJobTest < ActiveJob::TestCase
  include ActionCable::TestHelper

  test "broadcast message to room" do
    room = rooms(:all)

    assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
      ChatRelayJob.perform_now(room, "Hi!")
    end
  end
end

14.4 テストをCIで実行する

CI (Continuous Integration: 継続的インテグレーション) は開発手法の一種で、変更をこまめにメインのコードベースに統合し、マージ前に自動テストを実行します。

CI環境ですべてのテストを実行するのに必要なコマンドは、以下の1つだけです。

$ bin/rails test

システムテストは通常のテストよりもかなり遅いため、bin/rails testコマンドはシステムテストを実行しません。 システムテストを実行するには、CIステップにbin/rails test:systemの実行を追加するか、最初のステップをbin/rails test:allに変更して、システムテストを含むすべてのテストを実行するようにします。

15 並列テスト

並列テスト(parallel testing)を用いることで、テストスイート全体の実行に要する時間を削減できます。デフォルトの手法はプロセスのforkですが、スレッドもサポートされています。

15.1 プロセスを用いた並列テスト

デフォルトの並列化手法は、RubyのDRbシステムを用いるプロセスのforkです。プロセスは、提供されるワーカー数に基づいてforkされます。デフォルトのワーカー数は、実行されるマシンの実際のコア数ですが、parallelizeメソッドに数値を渡すことで変更できます。

並列化を有効にするには、test_helper.rbに以下を記述します。

class ActiveSupport::TestCase
  parallelize(workers: 2)
end

渡されたワーカー数は、プロセスがforkされる回数です。 ローカルテストスイートをCIとは別の方法で並列化したい場合は、以下の環境変数を用いてテスト実行時に指定するワーカー数を手軽に変更できます。

$ PARALLEL_WORKERS=15 bin/rails test

テストを並列化すると、Active Recordはデータベースの作成やスキーマのデータベースへの読み込みを自動的にプロセスごとに扱います。データベース名の末尾には、ワーカー数に応じた数値が追加されます。たとえば、ワーカーが2つの場合はtest-database-0test-database-1がそれぞれ作成されます。

渡されたワーカー数が1以下の場合はプロセスはforkされず、テストは並列化しません。テストのデータベースもオリジナルのtest-databaseのままになります。

並列テスト用に2つのフックが提供されます。これらのフックは、データベースを複数使っている場合や、ワーカー数に応じた他のタスクを実行する場合に便利です。

  • parallelize_setupメソッド: プロセスがforkした直後に呼び出されます。
  • parallelize_teardownメソッド: プロセスがcloseする直前に呼び出されます。
class ActiveSupport::TestCase
  parallelize_setup do |worker|
    # データベースをセットアップする
  end

  parallelize_teardown do |worker|
    # データベースをクリーンアップする
  end

  parallelize(workers: :number_of_processors)
end

これらのメソッドは、スレッドを用いる並列テストでは不要であり、利用できません。

15.2 スレッドを用いる並列テスト

スレッドを使いたい場合やJRubyを利用する場合のために、スレッドによる並列化オプションも提供されています。スレッドによる並列化では、minitestのParallel::Executorが使われます。

並列化手法をforkからスレッドに変更するには、test_helper.rbに以下を記述します。

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors, with: :threads)
end

JRubyで生成されたRailsアプリケーションには、自動的にwith: :threadsオプションが含まれます。

プロセスの場合と同様に、スレッドベースの並列テストでもPARALLEL_WORKERS環境変数を使ってテスト実行ワーカー数を変更できます。

15.3 並列トランザクションをテストする

並列トランザクションをスレッドで実行するコードをテストしたい場合、テスト用トランザクションの下ですでにネストされているため、トランザクションが互いにブロックしてしまう可能性があります。

これを回避するには、以下のようにself.use_transactional_tests = falseを設定することで、テストケースのクラスでトランザクションを無効にできます。

class WorkerTest < ActiveSupport::TestCase
  self.use_transactional_tests = false

  test "parallel transactions" do
    # トランザクションを作成するスレッドがいくつか起動される
  end
end

トランザクションを無効にしたテストでは、テストが完了しても変更が自動でロールバックされなくなります。そのため、テストで作成したデータをテスト実行後にすべてクリーンアップする必要があります。

15.4 テストを並列化するスレッショルドの指定

テストを並列実行すると、データベースのセットアップやフィクスチャの読み込みなどでオーバーヘッドが発生します。そのため、Railsはテスト件数が50未満の場合は並列化を行いません。

このスレッショルド(閾値)はtest.rbで以下のように設定できます。

config.active_support.test_parallelization_threshold = 100

以下のようにテストケースレベルでも並列化のスレッショルドを設定可能です。

class ActiveSupport::TestCase
  parallelize threshold: 100
end

16 eager loadingをテストする

通常、development環境やtest環境のアプリケーションは、高速化のためにeager loading(読み込みを遅延させずに一括で読み込むこと)を行うことはありませんが、production環境のアプリケーションはeager loadingを行います。

プロジェクトのファイルの一部が何らかの理由で読み込めない場合、production環境にデプロイする前にそのことを検出することが重要です。

16.1 CIの場合

プロジェクトでCI(Continuous Integration: 継続的インテグレーション)を利用している場合、アプリケーションでeager loadingを確実に行う手軽な方法の1つは、CIでeager loadingすることです。

CIでは、テストスイートがそこで実行されていることを示すために、以下のように何らかの環境変数(CIなど)を設定するのが普通です。

# config/environments/test.rb
config.eager_load = ENV["CI"].present?

Rails 7からは、新規生成されたアプリケーションでこの設定がデフォルトで行われます。

プロジェクトでCIを利用していない場合でも、以下のようにテストスイートでRails.application.eager_load!を呼び出すことでeager loadingを実行できます。

require "test_helper"

class ZeitwerkComplianceTest < ActiveSupport::TestCase
  test "eager loads all files without errors" do
    assert_nothing_raised { Rails.application.eager_load! }
  end
end

17 その他のテスティング関連リソース

17.1 エラー処理

Railsのシステムテスト、結合テスト、コントローラの機能テストでは、テスト中に発生した例外のrescueを試み、HTMLエラーページで応答します。この振る舞いはconfig.action_dispatch.show_exceptions設定で制御できます。

17.2 時間に依存するコードをテストする

Railsには、時間の影響を受けやすいコードが期待どおりに動作しているというアサーションに役立つ組み込みのヘルパーメソッドを提供しています。

以下の例ではtravel_toヘルパーを使っています。

# 登録後のユーザーは1か月分の特典が有効だとする
user = User.create(name: "Gaurish", activation_date: Date.new(2004, 10, 24))
assert_not user.applicable_for_gifting?
travel_to Date.new(2004, 11, 24) do
  assert_equal Date.new(2004, 10, 24), user.activation_date #`travel_to`ブロック内では`Date.current`がモック化される
  assert user.applicable_for_gifting?
end
assert_equal Date.new(2004, 10, 24), user.activation_date # この変更は`travel_to`ブロック内からしか見えない

時間関連のヘルパーについて詳しくは、ActiveSupport::Testing::TimeHelpers APIドキュメントを参照してください。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加