flash.nowの内容を直接テストすることは不可能

目指す例外ActiveRecord::StaleObjectErrorの発生を確認できたので、その状況を適切に表示するようにしたい。またしてもテストを先に書いてみる。そして、またしてもハマった...。

テスト

  • 欲しいメッセージが、flash[:notice]に入っていれば良いと考えた。
# functionalテスト: test/functional/slips_controller_test.rb
...(中略)...
class SlipsControllerTest < ActionController::TestCase
  def test_shoould_not_update_slip_when_slip_in_lock
    2.times do
      put :update, {:id     =>'1', 
                    :slip   =>{:number=>'1', :executed_on=>'2/14', :total_yen=>'1,000', :lock_version=>'0'}, 
                    :journal=>{"1"=>{:yen=>"1,000", :index=>"j943792544", :position=>"1", :comment=>"test"}
                              }
                   }
    end
    assert_response :success
    assert_template 'edit'
    assert_equal "他のユーザーが編集済のため修正できません。", flash[:notice] ###### 追加した行
  end
...(中略)...

実装

  • rescueの中で、例外によって処理を分岐してみた。
# コントローラー: app/controllers/slips_controller.rb
class SlipsController < ApplicationController
...(中略)...
  def update
    @slip = Slip.find(params[:id])
    @journals = @slip.make_journals(params[:journal])
    Slip.transaction do
      @slip.update_attributes!(params[:slip])
      # 保存前にDBのjournalを一旦クリアする。journalは毎回、新規作成される。クリアしないと二重登録になってしまう。
      @slip.journals.clear
      @journals.each {|journal| journal.save! if journal.input?}
      respond_to do |format|
        flash[:notice] = _('Slip was successfully updated.')
        format.html { redirect_to(@slip) }
        format.xml  { head :ok }
      end
    end

  rescue => error_obj
    case error_obj
    when ActiveRecord::StaleObjectError
      flash.now[:notice] = "他のユーザーが編集済のため修正できません。"
    else
      @journals.each {|journal| journal.valid? if journal.input?}
    end
    
    respond_to do |format|
      format.html { render :action => "edit" }
      format.xml  { render :xml => @slip.errors, :status => :unprocessable_entity }
    end
  end
...(中略)...

問題

  • またしても正しく実装できたのに、テストは常に失敗している...。flash[notice]が何故かnilのようだ。
  • ブラウザで確認すると、ちゃんとメッセージは表示されているのに。
 1) Failure:
 test_shoould_not_update_slip_when_slip_in_lock(SlipsControllerTest)
 [./test/functional/slips_controller_test.rb:96:in `test_shoould_not_update_slip_when_slip_in_lock'
 /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/testing/default.rb:7:in `run']:
 --- /var/folders/7t/7txMamB3F5yDjLEWm5P6ik+++TI/-Tmp-/expect.2983.0	2008-04-10 14:14:07.000000000 +0900
 +++ /var/folders/7t/7txMamB3F5yDjLEWm5P6ik+++TI/-Tmp-/butwas.2983.0	2008-04-10 14:14:07.000000000 +0900
 @@ -1 +1 @@
 -他のユーザーが編集済のため修正できません。
 +

原因

  • 原因は、「flash.now」にあった。
    • flashは次のアクション完了まで保持される特殊なsession変数。リダイレクト先でメッセージを表示したい時によく利用する。そして、flash.nowに代入すると、今のアクションが完了した時点で内容が破棄される。ほとんどインスタンス変数と同じ振る舞いだ。*1
  • あれれ、「assert_equal "他のユーザーが...", flash[:notice]」を実行する時点で、flash[:notice]は破棄されている可能性が...。
    • put :updateは処理を現場に投げて、「assert_response :success」「assert_template "edit"」が通っているので。

まさにその通りであった...。

解決

  • ということで、「assert_equal "他のユーザーが...", flash[:notice]」をテストすることは不可能。
  • でも、flash[:notice]はメッセージとして必ず描画されるので、ビューの中に目指すメッセージが含まれていればそれでOKなのだ。
  • というより、メッセージが表示されているかまで確認することの方が重要だ。以下のように修正してみた。
# functionalテスト: test/functional/slips_controller_test.rb
...(中略)...
class SlipsControllerTest < ActionController::TestCase
  def test_shoould_not_update_slip_when_slip_in_lock
    2.times do
      put :update, {:id     =>'1', 
                    :slip   =>{:number=>'1', :executed_on=>'2/14', :total_yen=>'1,000', :lock_version=>'0'}, 
                    :journal=>{"1"=>{:yen=>"1,000", :index=>"j943792544", :position=>"1", :comment=>"test"}
                              }
                   }
    end
    assert_response :success
    assert_template 'edit'
    # assert_equal "他のユーザーが編集済のため修正できません。", flash[:notice]
    assert_select "p", "他のユーザーが編集済のため修正できません。" ###### 追加した行
  end
...(中略)...

所感

これでテストは通った!それにしても、実装は簡単なのに、正しいテストを書けないためにそこでハマってしまう、その繰り返しだ。これでは何のためのテストなんだか...。でも、前向きに考えれば、以下のような効果も感じている。

  • 目指す機能を実装する前に、自分としては相当、論理的に考えるようになった。
    • 以前は欲しい機能を適当に想像しながら、途中で試行錯誤することが多かった。(今はテストで試行錯誤しているが...。)
    • 先にテストを書くには、目指す機能が明確である必要がある。その機能について、仕組みをよく考えるようになった。
  • Railsの仕様に少し詳しくなった気がする。
    • テストは、「assert_XXXX ...」つまり、「... であることに間違いない。」という真実を積み上げていくので、曖昧に理解していたことが真実として明らかになっていく気がする。
  • 修正した結果に自信が持てる。(一番のメリットかも。過去のテストもすべて通っているという安心感。)
  • テストは一つの機能を別のアプローチから表現しているようなもの。
    • 実装したコードは、設計図として内部の仕組みを表現している。
    • テストコードは、内部の仕組みはブラックボックスで、マニュアルとしてその使い方を表現している。

とりあえず、もう少し続けてみる。テストに慣れる必要がある。

*1:それでは、なぜインスタンス変数ではなく、flashを利用するのかと言えば、レイアウトファイルでは常に「flash[:notice]」でメッセージ表示を統一できて、余分な条件判定が不要で、シンプルに記述できるからだと思う。