本ガイドは、Railsでテストを書く方法について解説します。
このガイドの内容:
自動実行されるテストを書いておけば、ブラウザやコンソールでの手動テストよりも、コードが期待どおりに動作していることをより短時間で確認できます。テストが失敗すれば問題がすぐに明らかになり、開発プロセスの早い段階でバグを特定して修正できるようになります。テストの自動化によって、コードの信頼性が向上するだけでなく、変更に対する信頼性も向上します。
Railsでは、テストを簡単に記述できます。Railsに組み込まれているテストのサポートについては、次のセクションで詳しく説明します。
Railsでは、新しいアプリケーションの作成時からテストが開発プロセスの中心になります。
rails new アプリケーション名コマンドでRailsアプリケーションを作成すると、その場でtest/ディレクトリが作成されます。このディレクトリの内容を一覧表示すると、次のようになります。
$ ls -F test application_system_test_case.rb controllers/ helpers/ mailers/ system/ fixtures/ integration/ models/ test_helper.rb
testディレクトリの構造helpers/ディレクトリにはビューヘルパーのテスト、mailers/ディレクトリにはメーラーのテスト、models/ディレクトリにはモデル用のテストをそれぞれ保存します。
controllers/ディレクトリは、コントローラに関連するテストの置き場所で、ルーティングやビューに関連するテストもここに置きます。ここで行うテストではHTTPリクエストをシミュレートして、その結果に対してアサーションを行います。
integrationディレクトリは、コントローラ同士のやりとりのテストを置く場所として予約されています。
system/ディレクトリには、アプリケーションをブラウザから操作してテストするシステムテストが保存されます。システムテストを使うと、ユーザーが実際に体験する方法でアプリケーションをテストできるほか、JavaScriptのテストにも役立ちます。システムテストは、アプリケーションのブラウザ内テストを実行するCapybaraから継承した機能です。
フィクスチャはテストデータを編成する方法の1つであり、fixturesフォルダに置かれます。
フィクスチャは、テストで使うデータをモックアップする方法の一種です。フィクスチャを使うと、「実際の」データを使わずに済むようになります。これらはfixturesディレクトリに保存されます。詳しくはフィクスチャセクションで後述します。
ジョブテスト用のjobs/ディレクトリは、ジョブを最初に生成したときに作成されます。
test_helper.rbにはテスティングのデフォルト設定を記述します。
application_system_test_case.rbには、システムテストのデフォルトの設定を記述します。
デフォルトでは、すべてのRailsアプリケーションにはdevelopment環境、test環境、production環境の3つの環境があります。
それぞれの環境の設定はいずれも同様の方法で変更できます。テストの場合は、config/environments/test.rbにあるオプションを変更することでtest環境の設定を変更できます。
テストはRAILS_ENV=test環境で実行されます。これはRailsによって自動的に設定されます。
ガイドの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::Test(ActiveSupport::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かどうか1つのテストにアサーションを複数書くことも可能です。この場合、すべてのアサーションが成功しないとテストがパスしません。
今度はテストが失敗した場合の結果を見てみましょう。そのためには、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)と呼んでいます。
以下は、テスト中にエラーが発生すると、どのように表示されるかを確認するための、わざと誤りを含んだテストです。
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
これでテストはパスするはずです。
ここまでにいくつかのアサーションを紹介しました。アサーションは、テストの中心を担う重要な存在です。システムが計画通りに動作していることを実際に確認しているのはアサーションです。
以下は、Railsにデフォルトで組み込まれているテスティングライブラリであるminitestで使えるアサーションの抜粋です。
なお、[msg]パラメータは1個のオプション文字列メッセージであり、テストが失敗したときのメッセージをわかりやすくしたいときに指定できます。
assert( test, [msg] )testはtrueであると主張する。assert_not( test, [msg] )testはfalseであると主張する。assert_equal( expected, actual, [msg] )expected == actualはtrueであると主張する。assert_not_equal( expected, actual, [msg] )expected != actualはtrueであると主張する。assert_same( expected, actual, [msg] )expected.equal?(actual)はtrueであると主張する。assert_not_same( expected, actual, [msg] )expected.equal?(actual)はfalseであると主張する。assert_nil( obj, [msg] )obj.nil?はtrueであると主張する。assert_not_nil( obj, [msg] )obj.nil?はfalseであると主張する。assert_empty( obj, [msg] )objはempty?であると主張する。assert_not_empty( obj, [msg] )objはempty?ではないと主張する。assert_match( regexp, string, [msg] )assert_no_match( regexp, string, [msg] )assert_includes( collection, obj, [msg] )objはcollectionに含まれると主張する。assert_not_includes( collection, obj, [msg] )objはcollectionに含まれないと主張する。assert_in_delta( expected, actual, [delta], [msg] )expectedとactualの個数の差はdelta以内であると主張する。assert_not_in_delta( expected, actual, [delta], [msg] )expectedとactualの個数の差はdelta以内にはないと主張する。assert_in_epsilon ( expected, actual, [epsilon], [msg] )expectedとactualの個数の差がepsilonより小さいと主張する。assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )expectedとactualの数値にはepsilonより小さい相対誤差がないと主張する。assert_throws( symbol, [msg] ) { block }assert_raises( exception1, exception2, ... ) { block }assert_instance_of( class, obj, [msg] )objはclassのインスタンスであると主張する。assert_not_instance_of( class, obj, [msg] )objはclassのインスタンスではないと主張する。assert_kind_of( class, obj, [msg] )objはclassまたはそのサブクラスのインスタンスであると主張する。assert_not_kind_of( class, obj, [msg] )objはclassまたはそのサブクラスのインスタンスではないと主張する。assert_respond_to( obj, symbol, [msg] )objはsymbolに応答すると主張する。assert_not_respond_to( obj, symbol, [msg] )objはsymbolに応答しないと主張する。assert_operator( obj1, operator, [obj2], [msg] )obj1.operator(obj2)はtrueであると主張する。assert_not_operator( obj1, operator, [obj2], [msg] )obj1.operator(obj2)はfalseであると主張する。assert_predicate ( obj, predicate, [msg] )obj.predicateはtrueであると主張する(例:assert_predicate str, :empty?)。assert_not_predicate ( obj, predicate, [msg] )obj.predicateはfalseであると主張する(例:assert_not_predicate str, :empty?)。assert_error_reported(class) { block }assert_error_reported IOError { Rails.error.report(IOError.new("Oops")) })。assert_no_error_reported { block }assert_no_error_reported { perform_service })flunk( [msg] )これらはminitestがサポートするアサーションの一部に過ぎません。最新の完全なアサーションのリストについてはminitest APIドキュメント、特にMinitest::Assertionsを参照してください。
独自のアサーションを自作して利用することもできます。実際、Railsはまさにそれを行っているのです。Railsには開発を楽にしてくれる特殊なアサーションがいくつも追加されています。
アサーションの自作は高度なトピックなので、本ガイドでは扱いません。
Railsはminitestフレームワークに以下のような独自のカスタムアサーションを追加しています。
assert_difference(expressions, difference = 1, message = nil)
yieldされたブロックで評価された結果である式の戻り値における数値の違いをテストする。assert_no_difference(expressions, message = nil, &block)
assert_changes(expressions, message = nil, from:, to:, &block)
assert_no_changes(expressions, message = nil, &block)
assert_nothing_raised { block }
assert_recognizes(expected_options, path, extras = {}, message = nil)
expected_optionsハッシュで渡された(解析オプションがパスと一致したことを主張する。
基本的にこのアサーションでは、Railsがexpected_optionsで渡されたルーティングを認識していると主張する。assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
assert_recognizesと逆の動作)。
extrasパラメータは、クエリ文字列に追加リクエストがある場合にそのパラメータの名前と値をリクエストに渡すのに使われる。
messageパラメータにはアサーションが失敗した場合のカスタムエラーメッセージを渡せる。assert_routing(expected_path, options, defaults = {}, extras = {}, message = nil)
pathとoptionsが両方向に一致すると主張する。
つまり、pathがoptionsを生成し、次にoptionsがpathを生成することを検証する。
これは基本的にassert_recognizesとassert_generatesを1ステップに組み合わせたものである。
extrasハッシュを渡すと、アクションにクエリ文字列として提供されるオプションを指定できる。
messageパラメータを渡すと、失敗時に表示するカスタムエラーメッセージを指定できる。assert_response(type, message = nil)
:successを指定するとステータスコード200〜299を指定したことになり、同様に:redirectは300〜399、:missingは404、:errorは500〜599にそれぞれマッチする。
ステータスコードの数字や同等のシンボルを直接渡すこともできる。
詳しくはステータスコードの完全なリストおよびシンボルとステータスコードの対応リストを参照。assert_redirected_to(options = {}, message = nil)
assert_redirected_to root_pathなどの名前付きルートを渡すことも、assert_redirected_to @articleなどのActive Recordオブジェクトを渡すことも可能。assert_queries_count(count = nil, include_schema: false, &block)
&blockがSQLクエリのint数値を生成することを主張する。assert_no_queries(include_schema: false, &block)
&blockがSQLクエリを生成しないことを主張する。assert_queries_match(pattern, count: nil, include_schema: false, &block)
&blockが指定のパターンにマッチするSQLクエリを生成することを主張する。assert_no_queries_match(pattern, &block)
&blockが指定のパターンにマッチするSQLクエリを生成しないことを主張する。これらのアサーションのいくつかについては次の章で説明します。
Minitest::Assertionsに定義されているassert_equalなどの基本的なアサーションは、あらゆるテストケース内で用いられているクラスで利用できます。実際には、以下から継承したクラスもRailsで利用できます。
ActiveSupport::TestCaseActionMailer::TestCaseActionView::TestCaseActiveJob::TestCaseActionDispatch::Integration::SessionActionDispatch::SystemTestCaseRails::Generators::TestCase各クラスにはMinitest::Assertionsが含まれているので、どのテストでも基本的なアサーションを利用できます。
minitestについて詳しくは、minitestのドキュメントを参照してください。
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!
Railsアプリケーションのほとんどは、データベースと密接なやりとりを行うので、テストでもデータベースが必要となります。このセクションでは、このテストデータベースをセットアップしてサンプルデータを入力する方法について解説します。
test環境セクションで説明したように、すべてのRailsアプリケーションにはdevelopment環境、test環境、production環境の3つの環境があります。それぞれの環境におけるデータベース設定はconfig/database.ymlで行います。
テスティング専用のデータベースを用いることで、他の環境から切り離された専用のテストデータをセットアップしてアクセスできるようになります。これにより、development環境やproduction環境のデータベースにあるデータを気にすることなく、確実にテストを実行できます。
テストを実行するには、テスト用データベースで最新のスキーマが必要です。
テストヘルパーは、テストデータベースに未完了のマイグレーションが残っていないかどうかをチェックします。マイグレーションがすべて終わっている場合、db/schema.rbやdb/structure.sqlをテストデータベースに読み込みます。ペンディングされたマイグレーションがある場合、エラーが発生します。
このエラーが発生するということは、スキーマのマイグレーションが不完全であることを意味します。bin/rails db:migrate RAILS_ENV=testコマンドでテスト用データベースのマイグレーションを実行することで、スキーマが最新の状態になります。
既存のマイグレーションに変更が加えられていると、テストデータベースを再構築する必要があります。bin/rails test:dbを実行することで再構築できます。
よいテストを作成するにはよいテストデータを準備する必要があることを理解しておく必要があります。 Railsでは、フィクスチャを使ってテストデータの定義やカスタマイズを行えます。 網羅的なドキュメントについては、フィクスチャAPIドキュメントを参照してください。
フィクスチャ(fixture)とは、一貫したデータセットを表す専門用語です。フィクスチャを使うことで、事前に定義したデータをテスト実行直前にtestデータベースに導入できます。フィクスチャはYAMLで記述するので、特定のデータベースに依存しません。1つのモデルにつき1つのフィクスチャファイルが作成されます。
フィクスチャは、テストで必要なあらゆるオブジェクトを作成できるように設計されているわけではありません。一般的なテストケースに適用可能なデフォルトデータのみを用いるようにすることで、フィクスチャを最も効果的に管理できます。
フィクスチャファイルはtest/fixtures/ディレクトリの下に置かれます。
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_toとhas_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にある記事firstのcategoryキーの値が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ドキュメントを参照してください。
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::BlobとActiveStorage::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
ERBは、テンプレート内にRubyコードを埋め込むのに使われます。YAMLフィクスチャ形式のファイルは、Railsに読み込まれたときにERBによる事前処理が行われます。ERBを活用すれば、Rubyコードでサンプルデータを生成できます。たとえば、以下のコードを使えば1000人のユーザーを生成できます。
<% 1000.times do |n| %> user_<%= n %>: username: <%= "user#{n}" %> email: <%= "user#{n}@example.com" %> <% end %>
Railsはデフォルトで、test/fixturesフォルダにあるすべてのフィクスチャを自動的に読み込みます。フィクスチャの読み込みは主に以下の3つの手順で行われます。
Railsでは、データベースから既存のデータベースを削除するために外部キーやチェック制約といった参照整合性(referential integrity)トリガーを無効にしようとします。テスト実行時のパーミッションエラーが発生して困っている場合は、test環境のデータベースユーザーがこれらのトリガーを無効にする特権を持っていることを確認してください(PostgreSQLの場合、すべてのトリガーを無効にできるのはsuperuserのみです。PostgreSQLのパーミッションについて詳しくはこちらの記事を参照してください)。
フィクスチャは、実は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)
デフォルトの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
複数の書き込みデータベースが存在する場合、テストはそれらに対応する多数のトランザクションにラップされ、すべてがロールバックします。
以下のようにすることで、個別のテストケースでトランザクションを無効にできます。
class MyTest < ActiveSupport::TestCase # このテストケースではテストをデータベーストランザクションで暗黙にラップしなくなる self.use_transactional_tests = false end
モデルのテストは、アプリケーションのモデルとそれに関連するロジックをテストするときに使います。このロジックは、前述のセクションで説明したアサーションとフィクスチャでテストできます。
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を継承します。
機能テスト(functional test)を作成するときは、コントローラのアクションがリクエストと期待される結果(レスポンス)をどのように処理するかをテストすることに重点を置きます。 コントローラの機能テストは、システムテストが適していない場合(APIのレスポンスを確認する場合など)に使われることがあります。
機能テストでは以下のような項目をテストします。
機能テストの振る舞いを最も手軽に見る方法は、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つの引数を渡せます。
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リクエストとして呼び出し、paramsのtitleに新しいテキストを渡す。
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") }
HTTPリクエストに精通していれば、getがHTTPリクエストの一種であることも既に理解していることでしょう。Railsの機能テストでは以下の6種類のHTTPリクエストがサポートされています。
getpostpatchputheaddeleteこれらはすべてメソッドとして利用できます。典型的なCRUDアプリケーションでよく使われるのはget、post、put、deleteです。
機能テストでは、そのリクエストがアクションで受け付けられるかどうかを検証せず、代わりに結果を重視します。アクションが受け付けられるかどうかをテストするのであれば、リクエストテストの方が目的に合っています。
AJAX(Asynchronous JavaScript and XML)リクエストは、非同期HTTPリクエストを用いてサーバーからコンテンツを取得し、ページ全体を読み込まずにページの関連部分だけを更新する手法です。
get、post、patch、put、deleteメソッドで、以下のように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
リクエストが完了して処理されると、以下の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]も可
機能テストでは、リクエストが行われた後で以下の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
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" } # カスタム環境変数でリクエストをシミュレートする
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アクションに実装されています。
show、update、destroyアクションをテストするここまでは、: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を渡したり、メソッド名のシンボルを渡して呼び出したりできます。
結合テスト(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を継承しています。これにより、結合テスト内では標準のテストヘルパー以外にもさまざまなヘルパーを利用できます。
システムテストでは、標準のテストヘルパー以外にもActionDispatch::IntegrationTestから継承されるいくつかのヘルパーを利用できます。その中から3つのカテゴリについて簡単にご紹介します。
結合テストランナーについてはActionDispatch::Integration::Runnerを参照してください。
リクエストの実行についてはActionDispatch::Integration::RequestHelpersにあるヘルパーを用いることにします。
ファイルをアップロードする必要がある場合は、ActionDispatch::TestProcess::FixtureFileを参照してください。
セッションを改変する必要がある場合や、結合テストのステートを変更する必要がある場合は、ActionDispatch::Integration::Sessionを参照してください。
それではブログアプリケーションに結合テストを追加することにしましょう。最初に基本的なワークフローととして新しいブログ記事を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が表示されるはずなので、このアサーションはパスするはずです。
ブログに新しい記事を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!を必ず呼び出してください。
最後は、レスポンスが成功して記事がページ上で読める状態になっているというアサーションです。
ブログを表示して記事を1件作成するという、きわめて小規模なワークフローを無事テストできました。このテストにコメントを追加することも、記事の削除や編集のテストを行うこともできます。結合テストは、アプリケーションのあらゆるユースケースに伴うエクスペリエンスのテストに向いています。
システムテストはアプリケーションのユーザー操作のテストに使えます。テストは、実際のブラウザまたはヘッドレスブラウザに対して実行されます。システムテストではそのために背後で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ブラウザを用いて実行されます。次のセクションで、デフォルト設定の変更方法について説明します。
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などのリモートブラウザを使いたい場合は、以下のようにoptionsでbrowserにリモート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のドキュメントを参照してください。
それではアプリケーションにシステムテストを追加することにしましょう。システムテストで最初に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を実行してください。
それでは、新しい記事を作成するフローをテストしましょう。
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ページに表示されます。
デスクトップのテストに加えてモバイルのサイズもテストしたい場合は、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
以下は、Capybaraが提供している、システムテストで利用可能なアサーションの抜粋です。
assert_button(locator = nil, **options, &optional_filter_block)assert_current_path(string, **options)assert_field(locator = nil, **options, &optional_filter_block)assert_link(locator = nil, **options, &optional_filter_block)assert_selector(*args, &optional_filter_block)h1など)がページに存在することを主張する。assert_table(locator = nil, **options, &optional_filter_blockassert_text(type, text, **options)ScreenshotHelperは、テスト中のスクリーンショットをキャプチャするよう設計されたヘルパーで、テストが失敗した時点のブラウザ画面を確認するときや、デバッグでスクリーンショットを確認するときに有用です。
take_screenshotメソッドおよびtake_failed_screenshotメソッドが提供されており、take_failed_screenshotはRails内部のafter_teardownに自動的に含まれます。
take_screenshotヘルパーメソッドはテストのどこにでも書くことができ、ブラウザのスクリーンショット撮影に使えます。
システムテストの長所は、ユーザーによるやり取りをコントローラやモデルやビューを用いてテストできるという点で結合テストに似ていますが、本物のユーザーが操作しているかのようにテストを実際に実行できます。コメント入力や記事の削除、ドラフト記事の公開など、ユーザーがアプリケーションで行えるあらゆる操作をテストできます。
コードの重複を回避するために独自のテストヘルパーを追加できます。サインインヘルパーはその良い例です。
# 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
ヘルパーが増えてtest_helper.rbが散らかってきたことに気づいたら、別のファイルに切り出せます。切り出したファイルの置き場所はtest/libやtest/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
ヘルパーをtest_helper.rb内でeagerにrequireできると、テストファイルが暗黙でヘルパーにアクセスできるので便利です。これは以下のようなglob記法(*)で実現できます。
# test/test_helper.rb Dir[Rails.root.join("test", "test_helpers", "**", "*.rb")].each { |file| require file }
この方法のデメリットは、個別のテストで必要なファイルだけをrequireするよりも起動時間が長くなることです。
Railsアプリケーションの他のあらゆる部分と同様、ルーティングもテストできます。ルーティングのテストは、コントローラテストの一部としてtest/controllers/に配置します。
Railsで利用可能なルーティングアサーションについて詳しくは、ActionDispatch::Assertions::RoutingAssertionsのAPIドキュメントを参照してください。
アプリケーションのビューをテストする方法の1つは、リクエストに対するレスポンスをテストするために、あるページで重要なHTML要素とその内容がレスポンスに含まれているというアサーションを書くことです。ビューのテストも、ルーティングのテストと同様に、コントローラテストの一環としてtest/controllers/に配置します。
訳注: Rails 8のガイドでは、従来のassert_selectやassert_select_*などによるアサーションが、assert_domやassert_dom_*などに置き換えられました。
rails-dom-testing gemのassert_domやassert_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_elementをpageメソッドに変換できます。
# 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ベースのアサーションセクションを参照してください。
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には、デフォルトで以下のパーサーが定義されています。
:html: Nokogiri::XML::Nodeのインスタンスを返します:json: ActiveSupport::HashWithIndifferentAccessのインスタンスを返します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
以下は、ビューのテストで利用できるその他ののアサーションです。
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
パーシャルテンプレート(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
ヘルパー自体は単なるシンプルなモジュールであり、ビューから利用するヘルパーメソッドをこの中に定義します。
ヘルパーのテストについては、ヘルパーメソッドの出力が期待どおりであるかどうかをチェックするだけで十分です。ヘルパー関連のテストは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_toやpluralizeなどのRailsヘルパーメソッドにもアクセスできます。
メーラークラスを十分にテストするためには特殊なツールが若干必要になります。
Railsアプリケーションの他の部分と同様、メーラークラスについても期待どおり動作するかどうかをテストする必要があります。
メーラークラスをテストする目的は以下を確認することです。
メーラーのテストには単体テストと機能テストの2つの側面があります。
単体テストでは、完全に制御された入力を与えた結果の出力と、期待される既知の値(フィクスチャ)を比較します。
機能テストではメーラーによって作成される詳細部分についてのテストはほとんど行わず、コントローラとモデルがメーラーを正しく利用しているかどうかをテストするのが普通です。
メーラーのテストは、最終的に適切なメールが適切なタイミングで送信されたことを立証するために行います。
メーラーが期待どおりに動作しているかどうかをテストするために、事前に作成しておいた出力例と、メーラーの実際の出力を比較する形で単体テストを実行できます。
メーラーの単体テストを行なうには、フィクスチャを利用してメーラーが最終的に出力すべき外見の例を与えます。 これらのフィクスチャはメールのサンプル出力であり、通常のフィクスチャのようなActive Recordデータではないので、通常のフィクスチャとは別の専用のサブディレクトリに保存します。
test/fixtures/ディレクトリの下のディレクトリ名は、メーラーの名前に対応させます。たとえばUserMailerという名前のメーラーであれば、test/fixtures/user_mailerというスネークケースのディレクトリ名にします。
メーラーを生成しても、メーラーのアクションに対応するスタブフィクスチャは生成されません。これらのファイルは上述の方法で手動作成する必要があります。
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_sやemail.html_part.body.to_sを用いてそれぞれの一部に対するフィクスチャをテストできます。
inviteフィクスチャは以下のような内容にしておきます。
friend@example.comさん、こんにちは。 招待状を送付いたします。 どうぞよろしく!
テスト時の送信モードは、config/environments/test.rbのActionMailer::Base.delivery_method = :testという行で:testに設定されています。これにより、送信したメールが実際に配信されないようにできます。そうしないと、テスト中にユーザーにスパムメールを送りつけてしまうことになります。この設定で送信したメールは、実際にはActionMailer::Base.deliveriesという配列に追加されます。
このActionMailer::Base.deliveriesという配列は、ActionMailer::TestCaseとActionDispatch::IntegrationTestでのテストを除き、自動的にはリセットされません。それら以外のテストで配列をクリアしたい場合は、ActionMailer::Base.deliveries.clearで手動リセットできます。
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
単体テストはメールの属性をテストできますが、機能テストとシステムテストを使えば、配信するメールがユーザー操作によって適切にトリガーされているかどうかをテストできます。
たとえば、友人を招待する操作によってメールが適切に送信されたかどうかを以下のようにチェックできます。
# 結合テスト 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ドキュメントを参照してください。
複数のジョブを分離してテストする(ジョブの振る舞いに注目する)ことも、コンテキスト内でテストする(呼び出し元のコードの振る舞いに注目する)ことも可能です。
ジョブを生成すると、ジョブに関連するテストも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_jobsとperform_laterを使っています。こうすることで、リトライが設定されている場合には失敗したリトライが(再度エンキューされて無視されずに)テストでキャッチされるようになります。
コントローラなどで、呼び出しのたびにジョブが正しくエンキューされているかどうかをテストするのはよい方法です。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
特にリトライが設定されている場合は、特定のケースでジョブが例外を発生することをテストするのが難しくなることもあります。
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
この方法はフレームワークの一部(引数のシリアライズなど)を回避するため、一般には推奨されていません。
Action Cableはアプリケーション内部の異なるレベルで用いられるため、チャネル、コネクションのクラス自身、および他のエンティティがいずれも正しいメッセージをブロードキャストすることをテストする必要があります。
デフォルトでは、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ドキュメントを参照してください。
デフォルトでは、チャネルをジェネレータで生成するときに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ドキュメントを参照してください。
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
CI (Continuous Integration: 継続的インテグレーション) は開発手法の一種で、変更をこまめにメインのコードベースに統合し、マージ前に自動テストを実行します。
CI環境ですべてのテストを実行するのに必要なコマンドは、以下の1つだけです。
$ bin/rails test
システムテストは通常のテストよりもかなり遅いため、bin/rails testコマンドはシステムテストを実行しません。
システムテストを実行するには、CIステップにbin/rails test:systemの実行を追加するか、最初のステップをbin/rails test:allに変更して、システムテストを含むすべてのテストを実行するようにします。
並列テスト(parallel testing)を用いることで、テストスイート全体の実行に要する時間を削減できます。デフォルトの手法はプロセスのforkですが、スレッドもサポートされています。
デフォルトの並列化手法は、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-0とtest-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
これらのメソッドは、スレッドを用いる並列テストでは不要であり、利用できません。
スレッドを使いたい場合や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環境変数を使ってテスト実行ワーカー数を変更できます。
並列トランザクションをスレッドで実行するコードをテストしたい場合、テスト用トランザクションの下ですでにネストされているため、トランザクションが互いにブロックしてしまう可能性があります。
これを回避するには、以下のようにself.use_transactional_tests = falseを設定することで、テストケースのクラスでトランザクションを無効にできます。
class WorkerTest < ActiveSupport::TestCase self.use_transactional_tests = false test "parallel transactions" do # トランザクションを作成するスレッドがいくつか起動される end end
トランザクションを無効にしたテストでは、テストが完了しても変更が自動でロールバックされなくなります。そのため、テストで作成したデータをテスト実行後にすべてクリーンアップする必要があります。
テストを並列実行すると、データベースのセットアップやフィクスチャの読み込みなどでオーバーヘッドが発生します。そのため、Railsはテスト件数が50未満の場合は並列化を行いません。
このスレッショルド(閾値)はtest.rbで以下のように設定できます。
config.active_support.test_parallelization_threshold = 100
以下のようにテストケースレベルでも並列化のスレッショルドを設定可能です。
class ActiveSupport::TestCase parallelize threshold: 100 end
通常、development環境やtest環境のアプリケーションは、高速化のためにeager loading(読み込みを遅延させずに一括で読み込むこと)を行うことはありませんが、production環境のアプリケーションはeager loadingを行います。
プロジェクトのファイルの一部が何らかの理由で読み込めない場合、production環境にデプロイする前にそのことを検出することが重要です。
プロジェクトで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
Railsのシステムテスト、結合テスト、コントローラの機能テストでは、テスト中に発生した例外のrescueを試み、HTMLエラーページで応答します。この振る舞いはconfig.action_dispatch.show_exceptions設定で制御できます。
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ガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。