21. Turnip による受入テスト(3)と SimpleForm の設定

SimpleForm のインストール

旧システムで最もトラブっていたのは、ダブルクリックによるデータ二重化問題であった。検索していたところ、http://qiita.com/kakipo/items/d3fb5595ccfa7250f3eb というページを見つけた。旧システムでは form もリンク形式で表示することで、ユーザの統一感を持たせる形にしていたが、今回のシステムでは副作用でデータ生成が入る部分については、disable_with を設定するようにする。この記事では、SimpleForm という gem の submit の拡張としての記事になっているので、SimpleForm を調べてみた。

通常の form_for では label と実際の入力は自分で記述する必要があった。simple_form ではこのあたりはモデルを参照して locale から適切に設定をしてくれるとのことである。ラベルの表記がなくなるとかなり実装の手間が減る。早速 Gemfile に simple_form を追加し、bundle install する。

# simple form
gem 'simple_form', '~> 3.1.0'

その後、simple_form の generator を起動する。今回は Bootstrap を使っているので、--bootstrap オプションも記述する。

$ bin/rails g simple_form:install --bootstrap
[Simple Form] Simple Form is not configured in the application and will use the default values. Use `rails generate simple_form:install` to generate the Simple Form configuration.
      create  config/initializers/simple_form.rb
      create  config/initializers/simple_form_bootstrap.rb
       exist  config/locales
      create  config/locales/simple_form.en.yml
      create  lib/templates/haml/scaffold/_form.html.haml
===============================================================================

  Be sure to have a copy of the Bootstrap stylesheet available on your
  application, you can get it on http://getbootstrap.com/.

  Inside your views, use the 'simple_form_for' with one of the Bootstrap form
  classes, '.form-horizontal' or '.form-inline', as the following:

    = simple_form_for(@user, html: { class: 'form-horizontal' }) do |form|

===============================================================================

submit_tag の拡張

SimpleForm がインストールできたので、先の記事の submit_tag の拡張を行う。これは、data:{ disable_with:’xxx’ } を毎回記述しなくてよいようにするものである。config/initializers/simple_form.rb に以下の module を追記する。

# @see http://qiita.com/kakipo/items/d3fb5595ccfa7250f3eb
# @note form の二重送信を防ぐ
SimpleForm::FormBuilder.class_eval do
  def submit_with_override(value=nil, options={})
    value, options = nil, value if value.is_a?(Hash)
    data_disable_with = { disable_with: '送信中...' }
    options[:data] = data_disable_with.merge(options[:data] || {})
    submit_without_override(value, options)
  end
  alias_method_chain :submit, :override
end

index のテストを記述

現在の teacher/years はタイトルしか表示していないので、取得した年度一覧を表示する。まず、spec/controllers/teacher/years_controller.rb の spec を修正する。

  let(:year) { FactoryGirl.build(:year) }
  before do
    login_user_as :user_skyoumu, controller
  end

  describe "GET index" do
    before do
      @years = [ double(Year, year: 2014), double(Year) ]
      expect(Year).to receive(:order_year_desc) { @years }
      get :index
    end

    it { expect(response).to have_http_status(:success) }
    it("should have @years") { expect(assigns[:years]).to eq(@years) }
    it("should have a correct next_year instance") { expect(assigns[:next_year]).to eq(2015) }
  end

index の controller を記述

このテストを通すために、app/controllers/teacher/years_controller.rb を修正する。

  def index
    @navis = [ '年度変更' ]   
    @years = Year.order_year_desc
    @next_year = @years.first.year + 1
  end  

index の view を追加

一覧を描画するために app/views/teacher/years/index.html.haml にテーブルを追加する。現在設定されている年度は、info というクラスを設定することで目立たせている。

%h1 年度一覧                          
                                      
=simple_form_for :year, url: teacher_years_path do |f|
  %table.table.table-striped.table-hover.table-bordered
    %tr
      %th= t_ar 'Year.wareki'
      %th= t_ar 'Year.year'
      %th= t_ar 'Year.default_year'
    %tr
      %td{colspan: 3}
        =f.input :year, as: :hidden, input_html: { value: @next_year }
        =f.input :default_year, as: :hidden, input_html: { value: false }
        =f.button :submit, "#{Year.wareki_nendo(@next_year)}を作成", class: 'btn-block'
    - @years.each do |year|
      %tr{ class_info_or_nil(@now_year == year)}
        %td= year.wareki_nendo
        %td= year.year
        %td= mb year.default_year

helper を追加

ヘッダ部の t_ar は locale から翻訳文字列を取得するメソッドである。また、class_info_or_nil はフラグに従って、CSS class に ‘info’ を設定するメソッドである。mb はフラグに従って、○または×を出力するメソッドである。これらはすべてヘルパとして app/helpers/applicaiton_helper.rb に追記する。

  # @param [String] str locale キーワード
  # @return [String] 翻訳後の文字列
  # @see http://rails3try.blogspot.jp/2012/01/rails3-i18n.html?m=1
  def t_ar(str)
    model_str, atr = str.split(/\./)
    model = model_str.constantize
    if atr
      return model.human_attribute_name(atr)
    else
      return model.model_name.human
    end
  end

  # @param [Boolean] flag info class を設定するかどうか
  # @return [Hash] flag が true なら { class: 'info' }、false なら {}
  def class_info_or_nil(flag)
    flag ? { class: 'info' } : {}
  end

  # @param [Boolean] flag true or false
  # @return [String] true なら○、false なら×
  def mb(flag)
    flag ? '' : '×'
  end

locale データベースも追加すると、画面はこのようになる。

create のテストを追加

なお、表示ができたので受入テストも進み、以下のようなエラーとなる。

     Failure/Error: もし "平成27年度を作成"ボタンをクリックする
     AbstractController::ActionNotFound:
       The action 'create' could not be found for Teacher::YearsController

create メソッドを作成するために、spec/controllers/teacher/years_controller_spec.rb にテストを記述する。コントローラのテストでは基本的に mock と stub を使い、user 以外のデータベースは使わない。ここでのよくハマるポイントは、stub に渡すパラメータハッシュのキー。url を介して処理されるので、文字列になってしまう。stub ではシンボルだと一致しないことになる。

  describe "POST create" do
    before do
      @params = { 'year' => '2015', 'default_year' => false }
      @year = double(Year, wareki_nendo: '平成27年度')
      expect(Year).to receive(:new).with(@params) { @year }
    end

    context "when year.save is true" do
      before do
        expect(@year).to receive(:save) { true }
        post :create, year: @params
      end
      it { expect(response).to redirect_to(teacher_years_path) }
      it('should have a correct notice message') { expect(flash[:notice]).to eq('平成27年度を作成しました。') }
    end

    context "when year.save is false" do
      before do
        expect(@year).to receive(:save) { false }
        post :create, year: @params
      end
      it { expect(response).to redirect_to(teacher_years_path) }
      it('should have a correct alert message') { expect(flash[:alert]).to eq('平成27年度はすでに存在します。') }
    end
  end

create の実装を追加

このテストを通過させるための実装は以下のようになる。Rails 4 以降では strong parameters により params のクリーニングを行う必要がある。今回は year_params という private メソッドを使用している。

  def create
    year = Year.new(year_params)
    if year.save
      redirect_to teacher_years_path, notice: "#{year.wareki_nendo}を作成しました。"
    else
      redirect_to teacher_years_path, alert: "#{year.wareki_nendo}はすでに存在します。"
    end
  end

  private
  def year_params
    params.require(:year).permit(:year, :default_year)
  end

これでコントローラのテストは通過した。

ブラウザでのテスト

確認のためにブラウザで実行したところ、「平成30年度はすでに存在します。」というエラーが出てしまった。確認したところ、app/models/year.rb の validates の記述が間違っていたことを確認した。default_year は boolean なので、presence:true では検証できないことを失念していた。旧システムと同様に inclusion で validates するように修正した。

  validates :year, presence:true, uniqueness:true
  validates :default_year, inclusion: { in: [true, false] }

この結果、ブラウザでも正しく年度が追加できた。

初期データの巻き戻し

開発版データベースの Year テーブルに大量に年度を作成してしまったので、db:seed で元に戻すことにする。現在は、テーブルが空の場合だけデータ投入をしていたが、ファイルの日付が最近のものについては、読み込みをするように変更する。db/seeds.rb の該当部を以下のように修正する。

    recent = File::stat(fname).mtime > (DateTime.now - 300.0/86400)
    if storeClass.first == nil || recent

現在時刻より5分以内に更新されたファイルである場合には、テーブルが空でなくても読み込みを実施するようになった。テストで years.sql の時刻を更新し、db:seed を実行してみる。18個のデータが削除され、17個のデータが投入されており、正しく動作していることがわかる。

$ touch dump/years.sql; bin/rake db:seed
### load years
DELETE 18
COPY 17
18

長くなったので今日はここまで。

written by iHatenaSync