こんにちは、Misoca開発チームのmzpです。 先週は友人の家に泊り込んでWWDCのライブストリームを見ていました。
MisocaではRailsとJavaScriptでの値の共有にgonを利用していますが、ときおりcontrollerのテストが失敗するという奇妙な現象に遭遇しました。今日は、その話について書きたいと思います。
要約
テストケースごとに Gon.clear
を呼べば解決する。
もうちょっと長い要約
- gonはrequest store gemを使って値を保存する
- controllerのテスト内では、request storeは予期した動作をしない
- 明示的に
Gon.clear
を呼べば回避できる
gonとは
gonはRailsとJavaScriptで値を共有するためのgemです。
具体的には、Railsで
gon.user_role = "admin"
とのようにすると、JavaScriptで
gon.user_role // => "admin"
のように設定した値を取得できます。
どのように実現しているか
gonを利用するには、Viewのどこか(通常は app/views/layouts/application.html.erb
)に以下のようなコードを書く必要があります。 ここで、gon
オブジェクトへの値の設定を行なっています。
<%= Gon::Base.render_data %>
viewとcontrollerの間での値の共有は、RequestStoreというgemで実現されています。 これはグローバル変数のように、どこからでもアクセスできる領域を実現するためのgemです。
利用例:
def index RequestStore.store[:foo] ||= 0 RequestStore.store[:foo] += 1 render :text => RequestStore.store[:foo] end
gonはこのrequest storeに値を保持することで、controllerとviewの間でデータのやりとりをしています。
RequestStoreの仕組み
RequestStoreは Thread#[]
を利用したスレッドローカルなデータとほぼ同じです。
module RequestStore def self.store Thread.current[:request_store] ||= {} end def self.clear! Thread.current[:request_store] = {} end # snip
ただし、同一スレッド間別リクエストで値が保持されないようにRackのミドルウェアにてデータのクリアをしています。
module RequestStore class Middleware def initialize(app) @app = app end def call(env) @app.call(env) ensure RequestStore.clear! end end end
なにが問題になるか
controllerのテストはRackミドルウェアを経由していないため、予期したタイミングでデータがクリアされません。
そのため、以下のようなコードはproduction/development環境で動作する限りは問題ないですが、controllerのテストは失敗します。
class FooController < ApplicationController def foo # gonに不明なデータが残っていた場合は失敗させる fail "oops" if gon.some_data gon.some_data = 1 render text: 'ok' end def bar # gonに不明なデータが残っていた場合は失敗させる fail "oops" if gon.some_data gon.some_data = 2 render text: 'ok' end end
require 'test_helper' class FooControllerTest < ActionController::TestCase test "should get foo" do get :foo assert_response :success end test "should get zar" do get :bar assert_response :success end end
# それぞれはテストを通る $ TESTOPTS="-n /foo/" bundle exec rake test Run options: -n /foo/ --seed 49313 # Running tests: . Finished tests in 0.019805s, 50.4923 tests/s, 50.4923 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips $ TESTOPTS="-n /bar/" bundle exec rake test Run options: -n /bar/ --seed 7168 # Running tests: . Finished tests in 0.018819s, 53.1378 tests/s, 53.1378 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips # まとめて実行すると失敗する $ bundle exec rake test Run options: --seed 38769 # Running tests: .. Finished tests in 0.020623s, 96.9791 tests/s, 96.9791 assertions/s. 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
この例は比較的わかりやすいですが、実際はgonを使っているアクションと使っていないアクションがまざっていたり、テストケースの実行順をランダムにしていたりするので、たまに失敗する一見不可解な事象が発生します。
解決策
明示的に Gon.clear
をすれば問題を回避できます。
require 'test_helper' class FooControllerTest < ActionController::TestCase # snip def teardown Gon.clear end end
これで、テストケースの実行順に依存して成功したり、失敗したりするようなことはなくなりました。