2009-12-10 [長年日記]

_ [ruby][rails][AdventRubyJp] Railsで簡単にテストを増やす方法が使えなくなった件

(※12月の1日から25日まで、日替わりで Ruby の Tips を紹介するイベント、 Ruby Advent Calendar jp: 2009の 10 日目です。 昨日は「えいくん」こと @Sixeight さんでした。 明日は @ohac さんの予定です。)

最初に。Tips と言うか、長すぎてごめんなさい。まとめ力が足りないです。 空気を読まず Rails ネタです。

さて、ふつうの Rails つかいのみなさんは、もう 3.0 とかを先取りアピールしてて、 2-3-stable ってどんなんだっけ?てな感じかと思いますが、現場ではまだまだ 2.3 系も活躍しています。 Ruby と言えばアジャイル開発と言えば Ruby という感じで、コードを書く前にテストを書くなんて当たり前ですが、 今日はモデルを作るだけで 8 個もテストケースが書ける Tips をご紹介します。

$ rails _2.3.4_ 234
$ cd 234
234$ ruby script/generate model TestResult notes:text
234$ rake db:migrate

これだけです。2.3.4 を明示している理由は、後々分かります。 重要なのは、モデルのファイル名が test_*.rb となるようなクラス名を使うことです。 では、コードを書く前にテストを実行してみましょう。 「私は spec の方が……」なんて言わずに黙って rake test:units を実行するのが大人です。

234$ rake test:units
(snip)
8 tests, 1 assertions, 0 failures, 0 errors

ほら、もう 8 個もテストケースが書けています。ありきたりな User モデルでは、こうはいきません。 すべてのモデルを test_*.rb にすることによって、最初から 8 倍の生産性です!

……と喜んでいた時代が私にもありました。と言うのも、 先月(2009/11)末にリリースされた 2.3.5 では、この(姑息な)方法が使えなくなったのです。

$ rails _2.3.5_ 235
$ cd 235
235$ ruby script/generate model TestResult notes:text
235$ rake db:migrate
235$ rake test:units
(snip)
8 tests, 1 assertions, 0 failures, 1 errors
(snip)

ん?エラーが出ているようです。これでは 8 倍の生産性も台無しです。どうしてこうなった?

それではこの辺で、この生産性を叩き出す仕組みを詳しく見ていきましょう。 すでにお気づきかと思いますが、「テストケースが 8 個ある」、と言うだけで、 アサーションは 1 個しかありません。しかもそれは "test the truth" なので、なんのことはない、 どこかからテストケースが降ってきただけのことなのでした。以下のようにすれば確認できます。

235$ ruby -Itest test/unit/test_result_test.rb -v
Loaded suite test/unit/test_result_test
Started
test_results(ActionController::IntegrationTest): .
test_results(ActionController::TestCase): .
test_results(ActionMailer::TestCase): .
test_results(ActionView::TestCase): E
test_results(ActiveRecord::TestCase): .
test_results(ActiveSupport::TestCase): .
test_results(TestResultTest): .
test_the_truth(TestResultTest): .

Finished in 0.096729 seconds.

  1) Error:
test_results(ActionView::TestCase):
TypeError: wrong argument type Class (expected Module)
    

8 tests, 1 assertions, 0 failures, 1 errors

見たことあるようなクラス名が並んでいますね。さらに、定義もしていない test_results メソッドが実行されているようです。 そう、いまだに test/unit を愛用している人はお分かりかと思いますが、実はこれ、fixture accessor method なのです。 test/fixtures/test_results.yml などに

235$ cat test/fixtures/test_results.yml 
one:
  notes: MyText
two:
  notes: MyText

と書いてあると、test_results(:one) や test_results(:two) で各レコードのインスタンスにアクセスできるというアレです。 これが、例えばモデル名を TestResult にすることによって、fixture accessor method が test_results になり、 テストケースと誤認されるのです。

まとめると、以下のような流れです。

  • Test* というモデルがある
  • test/test_helper.rb で fixtures :all が呼ばれる
    • コンテキストは ActiveSupport::TestCase
  • fixture accessor method として test_* が定義される
    • Rails が用意する *::TestCase は たいてい ActiveSupport::TestCase を継承している
  • ユニットテストが実行される
  • ActionView::TestCase には setup のフックとして、クラス名からヘルパーを割り出して include する仕組みが用意された
    • でも割り出し方が不十分で、ActionView::TestCase 自身をヘルパーモジュールとして登録しようとして TypeError

これは、テストスクリプト内でしか使わない fixture accessor method を、 public メソッドとして定義している Rails のバグですね。

2.3.4 ではテストを増やす魔法のモデル名として使えていた Test* ですが、 2.3.5 からは一転して、使っちゃダメなモデル名となってしまいました。 テスト結果を格納するのに使うモデルの名前を TestResult 以外から選べ、というのは酷な話ですね。ヒドいよ!

さて、動作と原因が分かれば後は修正するだけですが、実はかなり前から報告はされていて、 あと一歩でコミットされそうというところまで来ているのですが、なぜか 2.3.5 には取り込まれませんでした。実質 1 行パッチなのに。 みなさんもこんなモデル名で困ったと言うのがあれば +1 の一声とともにチケットに追記してください。

Tips: Testなんちゃらというモデル名は使わない。

これだけではなんなので、対策方法を以下に示してこのエントリを締めたいと思います。

$ cat config/initializers/make_fixture_accessors_private.rb 
if Rails.env == "test" && %w[2.3.4 2.3.5].include?(Rails.version)
  require "active_record/fixtures"

  module ActiveRecord::TestFixtures::ClassMethods
    unless method_defined?(:setup_fixture_accessors_with_private)
      def setup_fixture_accessors_with_private(table_names = nil)
        setup_fixture_accessors_without_private(table_names).each do |table_name|
          private table_name
        end
      end

      alias_method_chain :setup_fixture_accessors, :private
    end
  end
end

Merry Christmas and Happy New Year!

[]

«前の日記(2009-09-19) 最新