Rails テスティングガイド

本ガイドは、アプリケーションをテストするためにRailsに組み込まれているメカニズムについて解説します。

このガイドの内容:

1 Railsアプリケーションでテストを作成しなければならない理由

Railsを使用すれば、テストをきわめて簡単に作成できます。テストの作成は、モデルやコントローラを作成する時点でテストコードのスケルトンを作成することから始まります。

Railsのテストが作成されていれば、後はそれを単に実行するだけで、特に大規模なリファクタリングを行なう際にコードが期待どおりに動作していることを即座に確認できます。

Railsのテストはブラウザのリクエストをシミュレートできるので、ブラウザを手動で操作せずにアプリケーションのレスポンスをテストできます。

2 テストを導入する

テスティングのサポートは最初期からRailsに組み込まれています。決して、最近テスティングが流行っていてクールだから導入してみた、というようなその場の思い付きで導入されたものではありません。Railsアプリケーションは、ほぼ間違いなくデータベースと密接なやりとりを行いますので、テスティングにもデータベースが必要となります。効率のよいテストを作成するには、データベースの設定方法とサンプルデータの導入方法を理解しておく必要があります。

2.1 test環境

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

テスティング専用のデータベースがあれば、それを設定して他の環境から切り離された専用のテストデータにアクセスすることができます。テストを実行すればテストデータは確実に元の状態から変わってしまうので、development環境やproduction環境のデータベースにあるデータには決してアクセスしません。

2.2 Railsを即座にテスト用に設定する

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にはテスティングのデフォルト設定を記入します。

2.3 フィクスチャのしくみ

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

2.3.1 フィクスチャとは何か

フィクスチャ (fixture)とは、いわゆるサンプルデータを言い換えたものです。フィクスチャを使用することで、事前に定義したデータをテスト実行直前にtestデータベースに導入することができます。フィクスチャはYAMLで記述され、特定のデータベースに依存しません。1つのモデルにつき1つのフィクスチャファイルが作成されます。

フィクスチャファイルはtest/fixturesの下に置かれます。rails generate modelを実行すると、モデルのフィクスチャスタブが自動的に作成され、このディレクトリに置かれます。

2.3.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

各フィクスチャは名前とコロンで始まり、その後にコロンで区切られたキー/値ペアのリストがインデント付きで置かれます。通常、レコード間は空行で区切られます。行の先頭に#文字を置くことで、フィクスチャファイルにコメントを追加できます。'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ドキュメントを参照してください。

2.3.3 ERB

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

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

Railsはデフォルトで、test/fixturesフォルダにあるすべてのフィクスチャを自動的に読み込み、モデルやコントローラのテストで使用します。フィクスチャの読み込みは主に以下の3つの手順からなります。

  • フィクスチャに対応するテーブルに含まれている既存のデータをすべて削除する
  • フィクスチャのデータをテーブルに読み込む
  • フィクスチャに直接アクセスしたい場合はフィクスチャのデータを変数にダンプする
2.3.5 フィクスチャはActive Recordオブジェクト

フィクスチャは、実はActive Recordのインスタンスです。前述の3番目の手順で示したように、フィクスチャはテストケースのローカル変数を自動的に設定してくれるので、フィクスチャのオブジェクトに直接アクセスできます。以下に例を示します。

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

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

# Userクラスで利用可能なメソッドにアクセスすることもできる
email(david.girlfriend.email, david.location_tonight)

3 モデルに対する単体テスト

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:「主張」を意味する) と呼ばれます。アサーションとは、オブジェクトまたは式を評価して、期待された結果が得られるかどうかをチェックするコードです。アサーションでは以下のようなチェックを行なうことができます。

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

1つのテストには必ず1つ以上のアサーションが含まれます。すべてのアサーションに成功してはじめてテストがパスします。

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

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

3.2 テストを実行する

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.rbtestディレクトリに置かれているので、このディレクトリは-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

3.3 単体テストに含めるべき項目

テストは、不具合が生じる可能性のあるあらゆる箇所に対して1つずつ含めることができれば理想的です。少なくとも、1つのバリデーションに対して1つ以上のテスト、1つのモデルに対して1つ以上のテストを作成することをお勧めします。

3.4 利用可能なアサーション

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

アサーションは非常に多くの種類が使用できるようになっています。 以下で紹介するのは、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] ) objempty?であると主張する。
assert_not_empty( obj, [msg] ) objempty?ではないと主張する。
assert_match( regexp, string, [msg] ) stringは正規表現 (regexp) にマッチすると主張する。
assert_no_match( regexp, string, [msg] ) stringは正規表現 (regexp) にマッチしないと主張する。
assert_includes( collection, obj, [msg] ) objcollectionに含まれると主張する。
assert_not_includes( collection, obj, [msg] ) objcollectionに含まれないと主張する。
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] ) objclassのインスタンスであると主張する。
assert_not_instance_of( class, obj, [msg] ) objclassのインスタンスではないと主張する。
assert_kind_of( class, obj, [msg] ) objclassまたはそのサブクラスのインスタンスであると主張する。
assert_not_kind_of( class, obj, [msg] ) objclassまたはそのサブクラスのインスタンスではないと主張する。
assert_respond_to( obj, symbol, [msg] ) objsymbolに応答すると主張する。
assert_not_respond_to( obj, symbol, [msg] ) objsymbolに応答しないと主張する。
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には開発を楽にしてくれる特殊なアサーションがいくつも追加されています。

アサーションの自作は高度なトピックなので、このチュートリアルでは扱いません。

3.5 Rails固有のアサーション

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

アサーション 目的
assert_difference(expressions, difference = 1, message = nil) {...} yieldされたブロックで評価された結果である式の戻り値における数値の違いをテストする。
assert_no_difference(expressions, message = nil, &amp;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) リクエストが適切なテンプレートファイルを使用してレンダリングされることを主張する。

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

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

Railsで1つのコントローラに含まれる複数のアクションをテストするには、コントローラに対する機能テスト (functional test) を作成します。コントローラはアプリケーションへのWebリクエストを受信し、最終的にビューをレンダリングしたものをレスポンスとして返します。

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

機能テストでは以下のようなテスト項目を実施する必要があります。

  • 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つの引数を渡すことができます。

  • リクエストの対象となる、コントローラ内のアクション。アクション名は文字列またはシンボルで指定できます。
  • アクションに渡すリクエストパラメータに含まれるオプションハッシュ1つ (クエリ文字列パラメータや記事の変数など)。
  • リクエストで渡されるセッション変数のオプションハッシュ1つ。
  • flashメッセージの値のオプションハッシュ1つ。

例: :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

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

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

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

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

これらはすべてメソッドとして利用できますが、実際には最初の2つでほとんどの用が足りるはずです。

機能テストは、そのリクエストがアクションで受け付けられるかどうかについては検証するものではありません。機能テストでこれらのリクエストの名前が使用されているのは、リクエストの種類を明示してテストを読みやすくするためです。

4.3 4つのハッシュ

6種類のメソッドのうち、getpostなどいずれかのリクエストが行われて処理されると、以下の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)

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

機能テストでは以下の3つの専用インスタンス変数を使用できます。

  • @controller - リクエストを処理するコントローラ
  • @request - リクエスト
  • @response - レスポンス

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

HTTPヘッダーCGI変数@requestインスタンス変数で直接設定できます。

# HTTPヘッダーを設定する
@request.headers["Accept"] = "text/plain, text/html"
get :index # ヘッダーをカスタマイズしたリクエストをシミュレートする

# CGI変数を設定する
@request.headers["HTTP_REFERER"] = "http://example.com/home"
post :create # 環境変数をカスタマイズしたリクエストをシミュレートする

4.6 テンプレートとレイアウトをテストする

レスポンスが出力するテンプレートとレイアウトが正しいかどうかを確認したい場合は、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キーを省略してしまうと期待どおりに動作しません。

4.7 完全な機能テストの例

flashassert_redirected_toassert_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

4.8 ビューをテストする

アプリケーションのビューのテストで、あるページで重要な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はきわめて強力なアサーションです。このアサーションの高度な利用法についてはドキュメントを参照してください。

4.8.1 その他のビューベースのアサーション

主にビューをテストするためのアサーションは他にもあります。

アサーション 目的
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

5 結合テスト

結合テスト (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から継承されます。これにより、結合テスト内でさまざまなヘルパーが利用できます。テストで使用するフィクスチャーも明示的に作成しておく必要があります。

5.1 結合テストで使用できるヘルパー

標準のテスト用ヘルパーの他に、結合テストで利用できるヘルパーを以下に示します。

ヘルパー 目的
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つ開く。

5.2 結合テストの例

複数のコントローラを対象にした単純な結合テストは、以下のようになります。

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

6 Rakeタスクでテストを実行する

作成したテストをひとつひとつ手動で実行する必要はありません。Railsにはテスティングを支援するためのコマンドが多数用意されています。Railsプロジェクトを生成したときのデフォルトのRakefileで利用できるすべてのコマンドを以下に示します。

タスク 説明
rake test すべての単体テスト、機能テスト、結合テストを実行します。Railsはデフォルトですべてのテストを実行するので、単にrakeを実行するだけで済みます。
rake test:controllers test/controllers以下にあるすべてのコントローラ用テストを実行します。
rake test:functionals test/controllerstest/mailerstest/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/modelstest/helpers、およびtest/unit以下にあるすべての単体テストを実行します。
rake test:all すべてのテストを素早く実行します (すべての種類のテストをマージし、dbのリセットは行わない)。
rake test:all:db すべてのテストを素早く実行します (すべての種類のテストをマージし、dbをリセットする)。

7 Minitestに関する簡単なメモ

Rubyには、テスティングを含むあらゆる一般的なユースケースのための膨大な標準ライブラリが付属しています。Ruby 1.9からはテスティングフレームワークとしてMinitestが提供されるようになりました。前述したassert_equalなどの基本的なアサーションは、実際にはすべてMinitest::Assertionsで定義されています。テストクラスで継承しているActiveSupport::TestCaseActionController::TestCaseActionMailer::TestCaseActionView::TestCaseおよびActionDispatch::IntegrationTestMinitest::Assertionsを含んでおり、これによってテスト内で基本的なアサーションがすべて利用できます。

Minitestの詳細についてはMinitestを参照してください。

8 SetupとTeardown

各テストの実行前に特定のコードブロックを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変数はどのテストでも利用できるようになります。setupteardownActiveSupport::Callbacksに実装されています。つまり、setupteardownはテストの中で単なるメソッドとして使用する以外の利用法もあるということです。これらを使用する際に以下のものを指定することができます。

  • ブロック1つ
  • メソッド1つ (前述の例のとおり)
  • シンボルで表されたメソッド名1つ
  • ラムダ1つ

前述の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

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

Railsアプリケーションの他の部分と同様、ルーティングもテストすることをお勧めします。前述のArticlesコントローラのデフォルトであるshowアクションへのルーティングをテストする例は以下のようになります。

test "should route to article" do
  assert_routing '/articles/1', {controller: "articles", action: "show", id: "1"}
end

10 メイラーをテストする

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

10.1 メイラーのテストについて

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

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

  • メールが処理 (作成および送信) されていること
  • メールの内容 (subject、sender、bodyなど) が正しいこと
  • 適切なメールが適切なタイミングで送信されていること
10.1.1 あらゆる側面からのチェック

メイラーのテストには単体テストと機能テストの2つの側面があります。単体テストでは、完全に制御された入力を与えた結果の出力と、期待される既知の値 (フィクスチャー) とを比較します。機能テストではメイラーによって作成される詳細部分についてのテストはほとんど行わず、コントローラとモデルがメイラーを正しく利用しているかどうかをテストするのが普通です。メイラーのテストは、最終的に適切なメールが適切なタイミングで送信されたことを立証するために行います。

10.2 単体テスト

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

10.2.1 フィクスチャーの逆襲

メイラーの単体テストを行なうために、フィクスチャーを利用してメイラーが最終的に出力すべき外見の例を与えます。これらのフィクスチャーはメールの出力例であって、通常のフィクスチャーのようなActive Recordデータではないので、通常のフィクスチャーとは別の専用のサブディレクトリに保存します。test/fixturesディレクトリの下のディレクトリ名は、メイラーの名前に対応させてください。たとえばUserMailerという名前のメイラーであれば、test/fixtures/user_mailerというスネークケースのディレクトリ名にします。

メイラーを生成すると、メイラーの各アクションに対応するスタブフィクスチャーが生成されます。Railsのジェネレータを使用しない場合は、自分でこれらのファイルを作成する必要があります。

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

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

10.3 機能テスト

メイラーの機能テストでは、メール本文や受取人が正しいことを確認するなど、単体テストでカバーされているようなことは扱いません。メールの機能テストでは、メール配信メソッドを呼び出し、その結果適切なメールが配信リストに追加されるかどうかをチェックします。機能テストでは配信メソッド自体は正常に動作すると仮定することになりますが、これでまず問題ありません。機能テストでは、期待されたタイミングでアプリケーションのビジネスロジックからメールが送信されるかどうかをテストすることがメインになるのが普通だからです。たとえば、友人を招待するという操作によってメールが適切に送信されるかどうかをチェックするには以下のような機能テストを使用します。

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

11 ヘルパーをテストする

ヘルパーのテストについては、ヘルパーメソッドの出力が期待どおりであるかどうかをチェックするだけで十分です。ヘルパー関連のテストは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_topluralizeなどのRailsヘルパーメソッドにアクセスできます。

12 その他のテスティングアプローチ

Railsアプリケーションではビルトインのminitestベースのテスティング以外のテストしか利用できないわけではありません。Rails開発者は以下のような実にさまざまなアプローチでテストの実行や支援を行っています。

  • NullDBはデータベースの利用を避けてテスティングを高速化する方法です。
  • Factory Girlはフィクスチャーに代わるテストデータ提供/生成ツールです。
  • Fixture Builderはテスト実行直前にRubyのファクトリーをコンパイルしてフィクスチャーに変換するツールです。
  • MiniTest::Spec Rails、RailsのテストではMiniTest::Spec DSLを利用します。
  • Shouldatest/unitを拡張してさまざまなヘルパー/マクロ/アサーションを追加します。
  • RSpecはビヘイビア駆動開発用のフレームワークです。