Rails 5 がリリースされました。多分目玉としては ActionCable の導入なのですが、既存コードベースのアップグレードに関して一番重要な問題は、コントローラーテストが廃止されるというものになるのではないでしょうか。
というわけで気持ちになってやっていきます。
一般的に今でも Rails のテストの記述には RSpec が用いられることが多いのではないでしょうか。僕も以前 RSpec の記法のメリットについて書きました。ですが私達のチームでは RSpec ではなく test-unit を使っています。理由としては
というわけですから私達のチームのテストコードは以下のような感じになっています。
require 'test_helper'
class PostControllerTest < ActionController::TestCase
test 'hogehoge' do
get(:hogehoge)
assert_response :ok
assert_equal hoge, assigns[:hoge]
end
test 'redirect_to hugahuga on hogehoge when hugahuga' do
get(:hogehoge, fugafuga: 'hohoho')
assert_redirected_to hugahuga_url
end
test 'payment_required on hogehoge when session has aaaa' do
get(:hogehoge, {}, {aaaa: true)
assert_response :payment_required
end
end
これをいちいちインテグレーションテストに書き換えるのは、テストファイル自体が 100 個ぐらいあってテストケースも 1000 個あるという場合にはなかなかやりたくない作業です。というわけですからこれを最低限の手間でやっていきたい。
まずは ActionController::TestCase を ActionDispatch::IntegrationTest に変えてしまいます。そのままでは当然テストが落ちます。そこでおもむろに ControllerIntegration というモジュールを作りこれを include します。
そして ControllerIntegration モジュールにコントローラーテストの各要素をインテグレーションテストに置き換える処理を書いていきます。実際にできたものがコレ。
この恐ろしい力技をごらんください。
# Integration テストでコントローラーテストを再現するためのメソッド群
module ControllerIntegration
extend ActiveSupport::Concern
attr_accessor :response_url, :response_session, :request, :controller
def session
obj = page.get_rack_session
obj.delete 'session_id'
obj
end
def do_request method, url, opts, session_obj
set_session session_obj
page.driver.options[:headers] = {}
page.driver.options[:headers]['HTTP_USER_AGENT'] = 'Rails Testing'
if @request and @request.env
@request.env.each do |k,v|
page.driver.options[:headers][k] = v
end
end
page.driver.submit(method, url, opts)
@response_url = current_url
@response_session = page.dup
@response = response()
end
%w(get post delete put patch).each do |name|
define_method name do |url, opts = {}, session_obj = {}|
if url.class.to_s == 'Symbol' and @controller
r = nil
r = @request.dup if @request
c = @controller.dup
url = url_for(opts.merge({controller: @controller, action: url}))
@controller = c
@request = r if r
end
do_request __method__, url, opts, session_obj
end
end
def set_session session_obj = {}
header = page.driver.options[:headers]
page.driver.options[:headers] = {}
page.set_rack_session(session_obj)
page.driver.options[:headers] = header
end
def response
obj = OpenStruct.new
obj.header = @response_session.response_headers
obj.headers = @response_session.response_headers
obj.body = @response_session.body
obj.status = @response_session.status_code
obj
end
def flash
page.get_rack_session['flash']['flashes'].symbolize_keys
end
def assert_response code, message = nil
case code
when :ok, :success
code = 200
when :not_found
code = 404
when :gone
code = 410
when :not_modified
code = 304
when :payment_required
code = 402
when :bad_request
code = 400
when :created
code = 201
end
assert_equal code, @response_session.status_code
end
def assert_redirected_to url
unless url =~ /http/
url = "http://www.example.com#{url}"
end
assert_equal url, @response_url
end
def assert_select selector, text = nil
if text
assert_selector selector, text: text
else
if text == false
assert css(selector).count == 0
else
assert css(selector).count > 0
end
end
end
def xpath xpath
Nokogiri::HTML(@response_session.body).xpath(xpath)
end
def css css
Nokogiri::HTML(@response_session.body).css(css)
end
def css_select css
css(css)
end
included do
setup{@request = OpenStruct.new(env: {})}
teardown do
Capybara.reset_sessions!
page.driver.options[:headers] = {}
end
end
end
これで各テスト内で setup{ @controller = 'コントローラー名'} とか書いてしまえばほぼそのままコントローラーテストがインテグレーションテストに化けます。ヒューリスティックにやっているのでもちろん書き換えは必要なのですが、あとはエディタのマクロとかでさくっと対処できます。
ただしこれでは assigns だけはどうにもなりません。 Capybara を使うインテグレーションテストはなにをどうしてもコントローラーの内部状態を見ることができないので、これはレスポンスをみるとかデータベースの状態の変化をみるとかそういうテストに書き換える必要があります。
コントローラーテストに assigns を大量使用していないという場合はこういう手法でわりと楽に Rails 5 に適合できるのではないでしょうか。
ちなみに上記コードを見ると分かるかと思いますが、私達のプロダクトでは 402 Payment Required が本当に使われています。
ところで株式会社ユビレジでは最新の Rails 環境で店舗運営を支える POS レジアプリを作りたいという開発者を募集しているそうです、今のところ募集フォームがないのでメールで募集する気概のあるというかたはよろしければどうぞ。