ssig33.com

Rails のコントローラーテストをインテグレーションテストに最低限の手間で移行する

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 レジアプリを作りたいという開発者を募集しているそうです、今のところ募集フォームがないのでメールで募集する気概のあるというかたはよろしければどうぞ。

back to index of texts


Site Search

Update History of this content