本ガイドは、アプリケーションをテストするためにRailsに組み込まれているメカニズムについて解説します。
このガイドの内容:
Railsを使用すれば、テストをきわめて簡単に作成できます。テストの作成は、モデルやコントローラを作成する時点でテストコードのスケルトンを作成することから始まります。
Railsのテストが作成されていれば、後はそれを単に実行するだけで、特に大規模なリファクタリングを行なう際にコードが期待どおりに動作していることを即座に確認できます。
Railsのテストはブラウザのリクエストをシミュレートできるので、ブラウザを手動で操作せずにアプリケーションのレスポンスをテストできます。
テスティングのサポートは最初期からRailsに組み込まれています。決して、最近テスティングが流行っていてクールだから導入してみた、というようなその場の思い付きで導入されたものではありません。Railsアプリケーションは、ほぼ間違いなくデータベースと密接なやりとりを行いますので、テスティングにもデータベースが必要となります。効率のよいテストを作成するには、データベースの設定方法とサンプルデータの導入方法を理解しておく必要があります。
デフォルトでは、すべてのRailsアプリケーションにはdevelopment、test、productionの3つの環境があります。それぞれの環境におけるデータベース設定はconfig/database.ymlで行います。
テスティング専用のデータベースがあれば、それを設定して他の環境から切り離された専用のテストデータにアクセスすることができます。テストを実行すればテストデータは確実に元の状態から変わってしまうので、development環境やproduction環境のデータベースにあるデータには決してアクセスしません。
rails new application_nameでRailsアプリケーションを作成すると、その場でtestフォルダが作成されます。このフォルダの内容は次のようになっています。
$ ls -F test controllers/ helpers/ mailers/ test_helper.rb fixtures/ integration/ models/
modelsディレクトリはモデル用のテストの置き場所であり、controllersディレクトリはコントローラ用のテストの置き場所です。integrationディレクトリは任意の数のコントローラとやりとりするテストを置く場所です。
フィクスチャはテストデータを編成する方法の1つであり、fixturesフォルダに置かれます。
test_helper.rbにはテスティングのデフォルト設定を記入します。
よいテストを作成するにはよいテストデータを準備する必要があることを理解しておく必要があります。 Railsでは、テストデータの定義とカスタマイズはフィクスチャで行うことができます。 網羅的なドキュメントについては、フィクスチャAPIドキュメントを参照してください。
フィクスチャ (fixture)とは、いわゆるサンプルデータを言い換えたものです。フィクスチャを使用することで、事前に定義したデータをテスト実行直前にtestデータベースに導入することができます。フィクスチャはYAMLで記述され、特定のデータベースに依存しません。1つのモデルにつき1つのフィクスチャファイルが作成されます。
フィクスチャファイルはtest/fixturesの下に置かれます。rails generate modelを実行すると、モデルのフィクスチャスタブが自動的に作成され、このディレクトリに置かれます。
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
各フィクスチャは名前とコロンで始まり、その後にコロンで区切られたキー/値ペアのリストがインデント付きで置かれます。通常、レコード間は空行で区切られます。行の先頭に#文字を置くことで、フィクスチャファイルにコメントを追加できます。'yes'や'no'などのYAMLキーワードに似たキーについては、引用符で囲むことでYAMLパーサーが正常に動作できます。
関連付けを使用している場合は、2つの異なるフィクスチャの間に参照ノードを1つ定義すれば済みます。belongs_to/has_many関連付けの例を以下に示します。
# fixtures/categories.ymlの内容: about: name: About # fixtures/articles.ymlの内容 one: title: Welcome to Rails! body: Hello world! category: about
名前で互いを参照する関連付けの場合、フィクスチャでid:属性を指定することはできません。Railsはテストの実行中に、一貫性を保つために自動的に主キーを割り当てます。フィクスチャでid:属性を指定するとこの自動割り当てがしなくなります。関連付けの詳細な動作については、フィクスチャAPIドキュメントを参照してください。
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つの手順からなります。
フィクスチャは、実はActive Recordのインスタンスです。前述の3番目の手順で示したように、フィクスチャはテストケースのローカル変数を自動的に設定してくれるので、フィクスチャのオブジェクトに直接アクセスできます。以下に例を示します。
# davidという名前のフィクスチャに対応するUserオブジェクトを返す users(:david) # idで呼び出されたdavidのプロパティを返す users(:david).id # Userクラスで利用可能なメソッドにアクセスすることもできる email(david.girlfriend.email, david.location_tonight)
Railsにおけるユニットテストとは、モデルをテストするために書いたコードを指します。
本ガイドでは、Railsの scaffold を使用します。scaffoldを使用すれば、1つの操作だけで新しいリソースのモデル、マイグレーション、コントローラ、ビューを一度に作成できます。このときに、Railsのベストプラクティスに準拠した完全なテストスイートも作成されます。本ガイドではこのようにして生成したコードを例として使用し、必要に応じてそこにコードを追加する形式で進めます。
Railsの scaffold の詳細については、Railsをはじめようを参照してください。
rails generate scaffoldを実行すると、生成される多数のファイルの中に、test/modelsフォルダの下に作成されるテストスタブがあります。
$ bin/rails generate scaffold 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はテストを実行するためのデフォルト設定を行なうためのファイルです。このファイルはどのテストにも必ずインクルードされるので、このファイルに追加したメソッドはすべてのテストで利用できます。
class ArticleTest < ActiveSupport::TestCase
ArticleTestクラスはActiveSupport::TestCaseを継承することによって、テストケース をひとつ定義しています。これにより、ActiveSupport::TestCaseのすべてのメソッドをArticleTestで利用できます。これらのメソッドのいくつかについてはこの後ご紹介します。
ActiveSupport::TestCaseのスーパークラスはMinitest::Testです。このMinitest::Testを継承したクラスで定義される、test_で始まるすべてのメソッドは単に「テスト」と呼ばれます。このtest_は小文字でなければなりません。従って、test_passwordおよびtest_valid_passwordというメソッド名は正式なテスト名となり、テストケースの実行時に自動的に実行されます。
Railsは、ブロックとテスト名をそれぞれ1つずつ持つtestメソッドを1つ追加します。この時生成されるのは通常のMinitest::Unitテストであり、メソッド名の先頭にtest_が付きます。次のようになります。
test "the truth" do assert true end
上のコードは、以下のように書いた場合と同等に振る舞います。
def test_the_truth assert true end
testマクロが適用されることによって、引用符で囲んだ読みやすいテスト名がテストメソッドの定義に変換されます。もちろん、後者のような通常のメソッド定義を使用することもできます。
テスト名からのメソッド名生成は、スペースをアンダースコアに置き換えることによって行われます。生成されたメソッド名はRubyの正規な識別子である必要はありません。テスト名にパンクチュエーション(句読点)などの文字が含まれていても大丈夫です。これが可能なのは、Rubyではメソッド名にどんな文字列でも使用できるようになっているからです。普通でない文字を使おうとするとdefine_method呼び出しやsend呼び出しが必要になりますが、名前の付け方そのものには公式な制限はありません。
assert true
上の行は アサーション (assertion:「主張」を意味する) と呼ばれます。アサーションとは、オブジェクトまたは式を評価して、期待された結果が得られるかどうかをチェックするコードです。アサーションでは以下のようなチェックを行なうことができます。
1つのテストには必ず1つ以上のアサーションが含まれます。すべてのアサーションに成功してはじめてテストがパスします。
テストを実行するには、テストデータベースが最新の状態で構成されている必要があります。テストヘルパーは、テストデータベースに未完了のマイグレーションが残っていないかどうかをチェックします。マイグレーションがすべて終わっている場合、db/schema.rbやdb/structure.sqlをテストデータベースに読み込みます。ペンディングされたマイグレーションがある場合、エラーが発生します。
rake testコマンドでテストケースを含むファイルを呼び出すことで、簡単にテストを実行できます。
$ bin/rake test test/models/article_test.rb . Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
テスト実行時にテストメソッド名を与えれば、テストケースに含まれる特定のテストメソッドだけを実行することもできます。
$ bin/rake test test/models/article_test.rb test_the_truth . Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
これにより、このテストケースに含まれるすべてのテストメソッドが実行されます。test_helper.rbはtestディレクトリに置かれているので、このディレクトリは-Iスイッチを使用して読み込みパスに追加しておく必要があります。
. (ドット) はテストにパスしたことを表します。テストに失敗すると代わりにFが表示され、エラー発生時にはEが表示されます。最後の行は実行結果の要約です。
今度はテストが失敗した場合の結果を見てみましょう。そのためには、article_test.rbテストケースに、確実に失敗するテストを以下のように追加してみます。
test "should not save article without title" do article = Article.new assert_not article.save end
それでは、新しく追加したテストを実行してみましょう。
$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title F Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. 1) Failure: test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: Failed assertion, no message given. 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
出力に含まれている単独の文字Fは失敗を表します。1)の後にこの失敗に対応するトレースが、失敗したテスト名とともに表示されています。次の数行はスタックトレースで、アサーションの実際の値と期待されていた値がその後に表示されています。デフォルトのアサーションメッセージには、エラー箇所を特定するのに十分な情報が含まれています。アサーションメッセージをさらに読みやすくするために、すべてのアサーションに以下のようにメッセージをオプションパラメータを渡すことができます。
test "should not save article without title" do article = Article.new assert_not article.save, "Saved the article without a title" end
テストを実行すると、以下のようにさらに読みやすいメッセージが表示されます。
1) Failure:
test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]:
Saved the article without a title
今度は title フィールドに対してモデルのレベルでバリデーションを行い、テストがパスするようにしてみましょう。
class Article < ActiveRecord::Base validates :title, presence: true end
このテストはパスするはずです。もう一度テストを実行してみましょう。
$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title . Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
お気付きになった方もいるかと思いますが、私たちは欲しい機能が未実装であるために失敗するテストをあえて最初に作成していることにご注目ください。続いてその機能を実装し、それからもう一度実行してテストがパスすることを確認しました。ソフトウェア開発の世界ではこのようなアプローチをテスト駆動開発 ( Test-Driven Development : TDD) と呼んでいます。
Rails開発者の多くがTDDを日々実践しています。この手法は、アプリケーションのあらゆる部品に対して動作試験を行なうためのテストスイートを作成する方法として非常に優れています。TDDそのものについては本ガイドの範疇を超えるためこれ以上言及しませんが、TDDを初めて学ぶための資料のひとつとしてRailsアプリケーションを開発するためのTDD 15ステップをご紹介します。
エラーがどのように表示されるかを確認するために、以下のようなエラーを含んだテストを作ってみましょう。
test "should report error" do # some_undefined_variable is not defined elsewhere in the test case some_undefined_variable assert true end
これで、このテストを実行するとさらに多くのメッセージがコンソールに表示されるようになりました。
$ bin/rake test test/models/article_test.rb test_should_report_error E: Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. 1) Error: test_should_report_error(ArticleTest): NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0> test/models/article_test.rb:10:in `block in <class:ArticleTest>' 1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
今度は'E'が出力されます。これはエラーが発生したテストが1つあることを示しています。
テストスイートに含まれる各テストメソッドは、エラーまたはアサーション失敗が発生するとそこで実行を中止し、次のメソッドに進みます。すべてのテストメソッドはアルファベット順に実行されます。
テストが失敗すると、それに応じたバックトレースが出力されます。Railsはデフォルトでバックトレースをフィルタし、アプリケーションに関連するバックトレースのみを出力します。これによって、フレームワークから発生する不要な情報を排除して作成中のコードに集中できます。完全なバックトレースを参照しなければならなくなった場合は、BACKTRACE環境変数を設定するだけで動作を変更できます。
$ BACKTRACE=1 bin/rake test test/models/article_test.rb
テストは、不具合が生じる可能性のあるあらゆる箇所に対して1つずつ含めることができれば理想的です。少なくとも、1つのバリデーションに対して1つ以上のテスト、1つのモデルに対して1つ以上のテストを作成することをお勧めします。
ここまでにいくつかのアサーションをご紹介しましたが、これらはごく一部に過ぎません。アサーションこそは、テストの中心を担う重要な存在です。システムが計画通りに動作していることを実際に確認しているのはアサーションです。
アサーションは非常に多くの種類が使用できるようになっています。
以下で紹介するのは、Minitestで使用できるアサーションからの抜粋です。MinitestはRailsにデフォルトで組み込まれているテスティングライブラリです。[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] ) |
stringは正規表現 (regexp) にマッチすると主張する。 |
assert_no_match( regexp, string, [msg] ) |
stringは正規表現 (regexp) にマッチしないと主張する。 |
assert_includes( collection, obj, [msg] ) |
objはcollectionに含まれると主張する。 |
assert_not_includes( collection, obj, [msg] ) |
objはcollectionに含まれないと主張する。 |
assert_in_delta( expecting, actual, [delta], [msg] ) |
expectedの個数とactualの個数の差分はdelta以内であると主張する。 |
assert_not_in_delta( expecting, actual, [delta], [msg] ) |
expectedの個数とactualの個数の差分はdelta以内にはないと主張する。 |
assert_throws( symbol, [msg] ) { block } |
与えられたブロックはシンボルをスローすると主張する。 |
assert_raises( exception1, exception2, ... ) { block } |
渡されたブロックから、渡された例外のいずれかが発生すると主張する。 |
assert_nothing_raised( 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_send( array, [msg] ) |
array[0]のオブジェクトがレシーバ、array[1]がメソッド、array[2以降]がパラメータである場合の実行結果はtrueであると主張する。(これは奇妙ではないか?) |
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_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_response(type, message = nil) |
レスポンスが特定のステータスコードを持っていることを主張する。:successを指定するとステータスコード200-299を指定したことになり、同様に:redirectは300-399、:missingは404、:errorは500-599にそれぞれマッチする。ステータスコードの数字や同等のシンボルを直接渡すこともできる。詳細についてはステータスコードの完全なリストおよびシンボルとステータスコードの対応リストを参照のこと。 |
assert_redirected_to(options = {}, message=nil) |
渡されたリダイレクトオプションが、最後に実行されたアクションで呼び出されたリダイレクトのオプションと一致することを主張する。このアサーションは部分マッチ可能。たとえばassert_redirected_to(controller: "weblog")はredirect_to(controller: "weblog", action: "show")というリダイレクトなどにもマッチする。assert_redirected_to root_pathなどの名前付きルートを渡したり、assert_redirected_to @articleなどのActive Recordオブジェクトを渡すこともできる。 |
assert_template(expected = nil, message=nil) |
リクエストが適切なテンプレートファイルを使用してレンダリングされることを主張する。 |
これらのアサーションのいくつかについては次の章でご説明します。
Railsで1つのコントローラに含まれる複数のアクションをテストするには、コントローラに対する機能テスト (functional test) を作成します。コントローラはアプリケーションへのWebリクエストを受信し、最終的にビューをレンダリングしたものをレスポンスとして返します。
機能テストでは以下のようなテスト項目を実施する必要があります。
ArticleリソースをRailsのscaffoldジェネレータで作成したので、コントローラとそのテストコードも自動的に作成されています。test/controllersディレクトリにarticles_controller_test.rbファイルができているはずなので、中がどのようになっているか見てみましょう。
articles_controller_test.rbファイルのtest_should_get_indexというテストについて見てみます。
class ArticlesControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success assert_not_nil assigns(:articles) end end
test_should_get_indexというテストでは、Railsがindexという名前のアクションに対するリクエストをシミュレートします。同時に、有効なarticlesインスタンス変数がコントローラに割り当てられます。
getメソッドはWebリクエストを開始し、結果をレスポンスとして返します。このメソッドには以下の4つの引数を渡すことができます。
例: :showアクションを呼び出し、idに12を指定してparamsとして渡し、セッションのuser_idに5を設定する。
get(:show, {'id' => "12"}, {'user_id' => 5})
別の例: :viewアクションを呼び出し、idに12を指定してparamsとして渡すが、セッションの代わりにflashメッセージを1つ使用する。
get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})
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 :create, article: {title: 'Some title'} end assert_redirected_to article_path(assigns(:article)) end
これで、すべてのテストを実行するとパスするようになったはずです。
HTTPリクエストに精通していれば、getがHTTPリクエストの一種であることも既に理解していることでしょう。Railsの機能テストでは以下の6種類のHTTPリクエストがサポートされています。
getpostpatchputheaddeleteこれらはすべてメソッドとして利用できますが、実際には最初の2つでほとんどの用が足りるはずです。
機能テストは、そのリクエストがアクションで受け付けられるかどうかについては検証するものではありません。機能テストでこれらのリクエストの名前が使用されているのは、リクエストの種類を明示してテストを読みやすくするためです。
6種類のメソッドのうち、getやpostなどいずれかのリクエストが行われて処理されると、以下の4種類のハッシュオブジェクトが使用できるようになります。
assigns - ビューで使用するためにアクションのインスタンス変数として保存されるすべてのオブジェクト。cookies - 設定されているすべてのcookies。flash - flash内のすべてのオブジェクト。session - セッション変数に含まれるすべてのオブジェクト。これらのハッシュは、通常のHashオブジェクトと同様に文字列をキーとして値を参照できます。シンボル名による参照も可能です (ただしassignsは除く)。たとえば次のようになります。
flash["gordon"] flash[:gordon] session["shmession"] session[:shmession] cookies["are_good_for_u"] cookies[:are_good_for_u] # Because you can't use assigns[:something] for historical reasons: assigns["something"] assigns(:something)
機能テストでは以下の3つの専用インスタンス変数を使用できます。
@controller - リクエストを処理するコントローラ@request - リクエスト@response - レスポンスHTTPヘッダーとCGI変数は@requestインスタンス変数で直接設定できます。
# HTTPヘッダーを設定する @request.headers["Accept"] = "text/plain, text/html" get :index # ヘッダーをカスタマイズしたリクエストをシミュレートする # CGI変数を設定する @request.headers["HTTP_REFERER"] = "http://example.com/home" post :create # 環境変数をカスタマイズしたリクエストをシミュレートする
レスポンスが出力するテンプレートとレイアウトが正しいかどうかを確認したい場合は、assert_templateメソッドを使用することができます。
test "index should render correct template and layout" do get :index assert_template :index assert_template layout: "layouts/application" end
テンプレートとレイアウトのテストは、一回のassert_template呼び出しで同時に行なうことはできない点にご注意ください。さらにlayoutのテストでは通常の文字列に代えて正規表現を与えることもできますが、文字列を使用する方がテストの内容が明確になります。一方、テストするレイアウトを指定する際には必ずレイアウトが置かれているディレクトリ名もパスに含めなければなりません。これはRails標準の"layouts"ディレクトリを使用している場合であっても必要です。従って
assert_template layout: "application"
上のコードは期待どおりに動作しません。
ビューでパーシャル (部分レイアウト) が使用されている場合は、レイアウトに対するアサーションを行なう際に必ずパーシャルにもアサーションを行なう必要があります。このとおりにしなかった場合、アサーションは失敗します。
従って
test "new should render correct layout" do get :new assert_template layout: "layouts/application", partial: "_form" end
上のコードでは_formというパーシャルを使用しているレイアウトに対して正しいアサーションが行われています。assert_templateで:partialキーを省略してしまうと期待どおりに動作しません。
flash、assert_redirected_to、assert_differenceを使用した別のテスト例を以下に示します。
test "should create article" do assert_difference('Article.count') do post :create, article: {title: 'Hi', body: 'This is my first article.'} end assert_redirected_to article_path(assigns(:article)) assert_equal 'Article was successfully created.', flash[:notice] end
アプリケーションのビューのテストで、あるページで重要なHTML要素とその内容がレスポンスに含まれていることを主張する (アサーションを行なう) のは、リクエストに対するレスポンスをテストする方法として便利です。assert_selectというアサーションを使用すると、こうしたテストで簡潔かつ強力な文法を利用できるようになります。
他のドキュメントでassert_tagというアサーションを見かけることがあるかもしれません。このアサーションはRails 4.2で削除されました。今後はassert_selectを使用してください。
assert_selectには2つの書式があります。
assert_select(セレクタ, [条件], [メッセージ])という書式は、セレクタで指定された要素が条件に一致することを主張します。セレクタにはCSSセレクタの式 (文字列) や代入値を持つ式を使用できます。
assert_select(要素, セレクタ, [条件], [メッセージ]) は、選択されたすべての要素が条件に一致することを主張します。選択される要素は、element (Nokogiri::XML::Node or Nokogiri::XML::NodeSetのインスタンス) からその子孫要素までの範囲から選択されます。
たとえば、レスポンスに含まれるtitle要素の内容を検証するには、以下のアサーションを使用します。
assert_select 'title', "Welcome to Rails Testing Guide"
ネストしたassert_selectブロックを使用することもできます。以下の例の場合、外側のassert_selectで選択されたすべての要素の完全なコレクションに対して、内側のassert_selectがアサーションを実行します。
assert_select 'ul.navigation' do assert_select 'li.menu_item' end
あるいは、外側のassert_selectで選択された要素のコレクションをイテレート (列挙) し、assert_selectが要素ごとに呼び出されるようにすることもできます。たとえば、レスポンスに2つの順序付きリストがあり、1つの順序付きリストにつき要素が4つあれば、以下のテストはどちらもパスします。
assert_select "ol" do |elements| elements.each do |element| assert_select element, "li", 4 end end assert_select "ol" do assert_select "li", 8 end
assert_selectはきわめて強力なアサーションです。このアサーションの高度な利用法についてはドキュメントを参照してください。
主にビューをテストするためのアサーションは他にもあります。
| アサーション | 目的 |
|---|---|
assert_select_email |
メールの本文に対するアサーションを行なう。 |
assert_select_encoded |
エンコードされたHTMLに対するアサーションを行なう。各要素の内容はデコードされた後にそれらをブロックとして呼び出す。 |
css_select(selector)またはcss_select(element, selector)
|
selectorで選択されたすべての要素を1つの配列にしたものを返す。2番目の書式については、最初にelementがベース要素としてマッチし、続いてそのすべての子孫に対してselectorのマッチを試みる。どちらの場合も、何も一致しなかった場合には空の配列を1つ返す。 |
assert_select_emailの利用例を以下に示します。
assert_select_email do assert_select 'small', 'オプトアウトしたい場合は "購読停止" をクリックしてください。' end
結合テスト (integration test) は、複数のコントローラ同士のやりとりをテストします。一般に、アプリケーション内の重要なワークフローのテストに使用されます。
結合テストは他の単体テストや機能テストと異なり、アプリケーションの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から継承されます。これにより、結合テスト内でさまざまなヘルパーが利用できます。テストで使用するフィクスチャーも明示的に作成しておく必要があります。
標準のテスト用ヘルパーの他に、結合テストで利用できるヘルパーを以下に示します。
| ヘルパー | 目的 |
|---|---|
https? |
セッションがセキュアなHTTPSリクエストを模倣している場合にtrueを返す。 |
https! |
セキュアなHTTPSリクエストを模倣できるようにする。 |
host! |
以後のリクエストでホスト名を設定できるようにする。 |
redirect? |
最後のリクエストがリダイレクトされた場合にtrueを返す。 |
follow_redirect! |
単一のリダイレクトレスポンスに従う。 |
request_via_redirect(http_method, path, [parameters], [headers]) |
HTTPリクエストを1つ作成し、以後のリダイレクトをすべて実行。 |
post_via_redirect(path, [parameters], [headers]) |
HTTP POSTリクエストを1つ作成し、以後のリダイレクトをすべて実行。 |
get_via_redirect(path, [parameters], [headers]) |
HTTP GETリクエストを1つ作成し、以後のリダイレクトをすべて実行。 |
patch_via_redirect(path, [parameters], [headers]) |
HTTP PATCHリクエストを1つ作成し、以後のリダイレクトをすべて実行。 |
put_via_redirect(path, [parameters], [headers]) |
HTTP PUTリクエストを1つ作成し、以後のリダイレクトをすべて実行。 |
delete_via_redirect(path, [parameters], [headers]) |
HTTP DELETEリクエストを1つ作成し、以後のリダイレクトをすべて実行。 |
open_session |
新しいセッションインスタンスを1つ開く。 |
複数のコントローラを対象にした単純な結合テストは、以下のようになります。
require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest test "login and browse site" do # HTTPSでログイン https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:products) end end
上のコード例で、結合テストに複数のコントローラが使用されていること、およびデータベースからディスパッチャまでスタック全体がテストされていることがおわかりいただけると思います。また、1つのテストの中で複数のセッションインスタンスを同時にオープンしたり、それらのインスタンスをアサーションメソッドで拡張することで、自分のアプリケーションだけに特化した非常に強力なテスティングDSLを作り出すこともできます。
結合テストで複数のセッションとカスタムDSLを使用した例を以下に示します。
require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest test "login and browse site" do # davidというユーザーがログイン david = login(:david) # ゲストユーザーがログイン guest = login(:guest) # どちらのユーザーも異なるセッションから利用できる assert_equal 'Welcome david!', david.flash[:notice] assert_equal 'Welcome guest!', guest.flash[:notice] # davidというユーザーはWebサイトをブラウズできる david.browses_site # ゲストユーザーもWebサイトをブラウズできる guest.browses_site # 他のアサーションに続く end private module CustomDsl def browses_site get "/products/all" assert_response :success assert assigns(:products) end end def login(user) open_session do |sess| sess.extend(CustomDsl) u = users(user) sess.https! sess.post "/login", username: u.username, password: u.password assert_equal '/welcome', sess.path sess.https!(false) end end end
作成したテストをひとつひとつ手動で実行する必要はありません。Railsにはテスティングを支援するためのコマンドが多数用意されています。Railsプロジェクトを生成したときのデフォルトのRakefileで利用できるすべてのコマンドを以下に示します。
| タスク | 説明 |
|---|---|
rake test |
すべての単体テスト、機能テスト、結合テストを実行します。Railsはデフォルトですべてのテストを実行するので、単にrakeを実行するだけで済みます。 |
rake test:controllers |
test/controllers以下にあるすべてのコントローラ用テストを実行します。 |
rake test:functionals |
test/controllers、test/mailers、test/functional以下にあるすべての機能テストを実行します。 |
rake test:helpers |
test/helpers以下にあるすべてのヘルパーテストを実行します。 |
rake test:integration |
test/integration以下にあるすべての結合テストを実行します。 |
rake test:mailers |
test/mailers以下にあるすべてのメーラーテストを実行します。 |
rake test:models |
test/models以下にあるすべてのモデルテストを実行します。 |
rake test:units |
test/models、test/helpers、およびtest/unit以下にあるすべての単体テストを実行します。 |
rake test:all |
すべてのテストを素早く実行します (すべての種類のテストをマージし、dbのリセットは行わない)。 |
rake test:all:db |
すべてのテストを素早く実行します (すべての種類のテストをマージし、dbをリセットする)。 |
Minitestに関する簡単なメモRubyには、テスティングを含むあらゆる一般的なユースケースのための膨大な標準ライブラリが付属しています。Ruby 1.9からはテスティングフレームワークとしてMinitestが提供されるようになりました。前述したassert_equalなどの基本的なアサーションは、実際にはすべてMinitest::Assertionsで定義されています。テストクラスで継承しているActiveSupport::TestCase、ActionController::TestCase、ActionMailer::TestCase、ActionView::TestCaseおよびActionDispatch::IntegrationTestはMinitest::Assertionsを含んでおり、これによってテスト内で基本的なアサーションがすべて利用できます。
Minitestの詳細についてはMinitestを参照してください。
各テストの実行前に特定のコードブロックを1つ実行したり、各テストの実行後に別のコードブロックを1つ実行したりしたい場合は、そのための特殊なコールバックを使用することができます。Articlesコントローラの機能テストを例にとって詳しく見てみましょう。
require 'test_helper' class ArticlesControllerTest < ActionController::TestCase # 各テストが実行される直前に呼び出される def setup @article = articles(:one) end # 各テストの実行直後に呼び出される def teardown # @article変数は実際にはテストの実行のたびに直前で初期化されるので # ここで値をnilにする意味は実際にはないのですが、 # teardownメソッドの動作を理解いただくためにこのようにしています @article = nil end test "should show article" do get :show, id: @article.id assert_response :success end test "should destroy article" do assert_difference('Article.count', -1) do delete :destroy, id: @article.id end assert_redirected_to articles_path end end
上のsetupメソッドは各テストの実行直前に呼び出されるので、@article変数はどのテストでも利用できるようになります。setupとteardownはActiveSupport::Callbacksに実装されています。つまり、setupとteardownはテストの中で単なるメソッドとして使用する以外の利用法もあるということです。これらを使用する際に以下のものを指定することができます。
前述のsetupコールバックで、メソッド名をシンボルで指定した場合の例を見てみましょう。
require 'test_helper' class ArticlesControllerTest < ActionController::TestCase # 各テストが実行される直前に呼び出される setup :initialize_article # 各テストの実行直後に呼び出される def teardown @article = nil end test "should show article" do get :show, id: @article.id assert_response :success end test "should update article" do patch :update, id: @article.id, article: {} assert_redirected_to article_path(assigns(:article)) end test "should destroy article" do assert_difference('Article.count', -1) do delete :destroy, id: @article.id end assert_redirected_to articles_path end private def initialize_article @article = articles(:one) end end
Railsアプリケーションの他の部分と同様、ルーティングもテストすることをお勧めします。前述のArticlesコントローラのデフォルトであるshowアクションへのルーティングをテストする例は以下のようになります。
test "should route to article" do assert_routing '/articles/1', {controller: "articles", action: "show", id: "1"} end
メーラークラスを十分にテストするためには特殊なツールが若干必要になります。
Railsアプリケーションの他の部分と同様、メーラークラスについても期待どおり動作するかどうかをテストする必要があります。
メーラークラスをテストする目的は以下を確認することです。
メーラーのテストには単体テストと機能テストの2つの側面があります。単体テストでは、完全に制御された入力を与えた結果の出力と、期待される既知の値 (フィクスチャー) とを比較します。機能テストではメーラーによって作成される詳細部分についてのテストはほとんど行わず、コントローラとモデルがメーラーを正しく利用しているかどうかをテストするのが普通です。メーラーのテストは、最終的に適切なメールが適切なタイミングで送信されたことを立証するために行います。
メーラーが期待どおりに動作しているかどうかをテストするために、事前に作成しておいた出力例と、メーラーの実際の出力を比較するという単体テストを行なうことができます。
メーラーの単体テストを行なうために、フィクスチャーを利用してメーラーが最終的に出力すべき外見の例を与えます。これらのフィクスチャーはメールの出力例であって、通常のフィクスチャーのようなActive Recordデータではないので、通常のフィクスチャーとは別の専用のサブディレクトリに保存します。test/fixturesディレクトリの下のディレクトリ名は、メーラーの名前に対応させてください。たとえばUserMailerという名前のメーラーであれば、test/fixtures/user_mailerというスネークケースのディレクトリ名にします。
メーラーを生成すると、メーラーの各アクションに対応するスタブフィクスチャーが生成されます。Railsのジェネレータを使用しない場合は、自分でこれらのファイルを作成する必要があります。
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).deliver_now assert_not ActionMailer::Base.deliveries.empty? # 送信されたメールの本文が期待どおりの内容であるかどうかをテスト 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ヘルパーを使用してこのファイルの内容を読みだしています。
inviteフィクスチャーは以下のような内容にしておきます。
friend@example.comさん、こんにちは。 招待状を送付いたします。 どうぞよろしく!
ここでメーラーのテスト作成方法の詳細部分についてご説明したいと思います。config/environments/test.rbのActionMailer::Base.delivery_method = :testという行で送信モードをtestに設定しています。これにより、送信したメールが実際に配信されないようにできます。そうしないと、テスト中にユーザーにスパムメールを送りつけてしまうことになります。この設定で送信したメールは、ActionMailer::Base.deliveriesという配列に追加されます。
このActionMailer::Base.deliveriesという配列は、ActionMailer::TestCaseでのテストを除き、自動的にはリセットされません。Action Mailerテストの外で配列をクリアしたい場合は、ActionMailer::Base.deliveries.clearで手動リセットできます。
メーラーの機能テストでは、メール本文や受取人が正しいことを確認するなど、単体テストでカバーされているようなことは扱いません。メールの機能テストでは、メール配信メソッドを呼び出し、その結果適切なメールが配信リストに追加されるかどうかをチェックします。機能テストでは配信メソッド自体は正常に動作すると仮定することになりますが、これでまず問題ありません。機能テストでは、期待されたタイミングでアプリケーションのビジネスロジックからメールが送信されるかどうかをテストすることがメインになるのが普通だからです。たとえば、友人を招待するという操作によってメールが適切に送信されるかどうかをチェックするには以下のような機能テストを使用します。
require 'test_helper' class UserControllerTest < ActionController::TestCase test "invite friend" do assert_difference 'ActionMailer::Base.deliveries.size', +1 do post :invite_friend, email: 'friend@example.com' end invite_email = ActionMailer::Base.deliveries.last assert_equal "You have been invited by me@example.com", invite_email.subject assert_equal 'friend@example.com', invite_email.to[0] assert_match(/Hi friend@example.com/, invite_email.body.to_s) end end
ヘルパーのテストについては、ヘルパーメソッドの出力が期待どおりであるかどうかをチェックするだけで十分です。ヘルパー関連のテストはtest/helpersディレクトリに置かれます。
ヘルパーテストの外枠は以下のような感じです。
require 'test_helper' class UserHelperTest < ActionView::TestCase end
ヘルパー自体は単なるモジュールであり、ビューから利用するヘルパーメソッドをこの中に定義します。ヘルパーメソッドの出力をテストするには、以下のようなミックスインを使用する必要があるでしょう。
class UserHelperTest < ActionView::TestCase include UserHelper test "should return the user name" do # ... end end
さらに、テストクラスはActionView::TestCaseをextendしたものなので、link_toやpluralizeなどのRailsヘルパーメソッドにアクセスできます。
Railsアプリケーションではビルトインのminitestベースのテスティング以外のテストしか利用できないわけではありません。Rails開発者は以下のような実にさまざまなアプローチでテストの実行や支援を行っています。
test/unitを拡張してさまざまなヘルパー/マクロ/アサーションを追加します。Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。