Thanks Driven Life

日々是感謝

Capybara+Turnip でシナリオ毎にセッションがリセットされて欲しくない

経緯

Capybara + Turnip を書く時、だいたいこういう Feature になると思います

Feature: GitHub を巡る

    Background:
      When "https://github.com/login" にアクセスする
       And ユーザ名に "gongo" と入力する
       And パスワードに "gongo" と入力する
       And "Sign in" ボタンをクリックする

    Scenario: トップ画面
      Then アクティビティが表示されていること
       And 自分のリポジトリ一覧が表示されていること

    Scenario: Explorer
      When "Explorer" をクリックする
      Then "Browse interesting projects" と表示されていること

    Scenario: Blog
      When "Blog" をクリックする
      Then "The GitHub Blog" と表示されていること

    Scenario: Gist
      When "Gist" をクリックする
      Then 自分の Gist が表示されていること

なぜ Background を使ってシナリオの前に毎回ログインさせているのかというと、 Capybara + Turnip で動かすと Scenario 終了後に Capybara.reset_sessions! が走るから です。

capybara/lib/capybara/rspec.rb#L18-L23

これはこれで特に問題ない動きかなと思いますが、シナリオ毎にログインするの無駄じゃね? と思うことが稀によくあります。

願望

ログイン処理、そこそこ重い部類に入るんじゃないかと思ってて、シナリオの数が増えれば増えるほどになかなか無視できない実行時間を生み出していきます。もし Capybara.reset_sessions! が毎シナリオで走らなかった場合、つまり「一度ログインしたらそのまま」という状況が実現できれば、さきほどの Feature は以下のように直せるのではないでしょうか

Feature: GitHub を巡る

    Scenario: ログインしてトップ画面を表示する
      When "https://github.com/login" にアクセスする
       And ユーザ名に "gongo" と入力する
       And パスワードに "gongo" と入力する
       And "Sign in" ボタンをクリックする
      Then アクティビティが表示されていること
       And 自分のリポジトリ一覧が表示されていること

    Scenario: Explorer
      When "Explorer" をクリックする
      Then "Browse interesting projects" と表示されていること

    Scenario: Blog
      When "Blog" をクリックする
      Then "The GitHub Blog" と表示されていること

    Scenario: Gist
      When "Gist" をクリックする
      Then 自分の Gist が表示されていること

ログイン処理は最初のみ。それ以降のシナリオは続けて実行しても問題ないためそのままに。 これで単純にログイン処理3回分の実行時間を削減できました。

もちろん全シナリオに渡ってこのような書き方をできるわけではなく、「このシナリオに入ったらログインやりなおしたいな」とか「ここからはセッションリセットしても大丈夫だ」みたいなのが出てくるわけです。それも制御したい!!

解決策(ボツ)

RSpec.configuration.after に登録 されてしまってからでは遅いので、 まずは capybara/rspec を require しない形を考えます。簡単に言えば上記コードの Capybara.reset_sessions! 以外を helper か何かに書いてしまえばいいのです。

ですがそれはめんどくさい。今後 capybara/rspec.rb に修正が加えられることになればそれに追従していかなければなりませんし、なにより turnip/capybara.rbcapybara/rspec を require してしまっています。 じゃあ今度は turnip/capybara を require しないで〜とか考えると、また気をつけないといけないコードが増えます。それはもうだめだ。

解決策(本命)

じゃあモンキーパッチですよね。Capybara.reset_session! だけを対象に、最低限に済ませます。

module Capybara
  class << self
    alias_method :original_reset_sessions!, :reset_sessions!
    def reset_sessions!
      # Noop
    end

    def reset_sessions_of_truth!
      original_reset_sessions!
    end
  end
end

これでひとまず各シナリオ終了後に呼ばれる reset_sessions! では何も起きなくなります。 次に Feature 開始時に reset_session! を呼ぶ という RSpec.configuration.before を設定します。

RSpec.configure do |config|
  config.before :all, type: :feature do
    Capybara.reset_sessions_of_truth!
  end
end

Turnip の設定により、Feature (実体は RSpec の ExampleGroup) が持つ metadata に :feature が設定されているので (※) 、type: :feature と記述することで Feature に対する before/after が記述できます。

上記の場合だと before :all, type: :feature なので、Feature が持つ Scenario (describe に対する it みたいな位置付け) の最初の1回だけセッションリセットを行う、ということになります。

せっかくなので「このシナリオに入ったらログインやりなおしたいな」も実現しましょう。

RSpec.configure do |config|
  config.before :each, type: :feature, reset_session: true do
    Capybara.reset_sessions_of_truth!
  end
end

Turnip では Feature や Scenario に対してタグ(@foo)を設定すると、その項目の metadata に foo: true がセットされます。 つまり :each, type: :feature, reset_session: true と書くことで @reset_session タグがつけられたシナリオが開始される前 という処理が記述できます。

結果

各 Feature の最初の Scenario の時だけ Capybara.reset_session! が処理されるようになった。 もし「このシナリオではセッションリセットしたいわ」という記述をしたい場合には

Feature: GitHub を巡る

    Scenario: ログインしてトップ画面を表示する
      When "https://github.com/login" にアクセスする
       And ユーザ名に "gongo" と入力する
       And パスワードに "gongo" と入力する
       And "Sign in" ボタンをクリックする
      Then アクティビティが表示されていること
       And 自分のリポジトリ一覧が表示されていること

    Scenario: まだログインしてる状態
      When "https://github.com" にアクセスする
      Then アクティビティが表示されていること

    @reset_session
    Scenario: ログアウト状態
      When "https://github.com" にアクセスする
      Then "Sign up for GitHub" というボタンが表示されていること

といった使い方ができるようになりました。

まとめ

Turnip に限ったことではなく、Capybara + RSpec でも似たようなことはできるようになると思います。

※ 昔、これと同じようなの実現したぜ紹介記事どっかで見た気もしますが、せっかくなので挙げました。車輪は回る。