31. コントローラのないモデルの移植

学年・学科など基本的に固定なデータのモデルは、特にコントローラを作成しない。これらを一括でテストおよび実装を移植する。

学年(Gakunen)モデルの定義

学年のマイグレーションは以下のようになる。honka = true に対して、gakunen が 1〜5、honka = false に対して gakunen が 1〜2 を取り得る。パターンは 7 通り。gakunen, honka の別に sort_order という属性があり、これを使ってソートする。Rails 4.2 になってから、generator で timestamps に null: false が設定されるようになっていた。これに対する対策は後で述べる。

class CreateGakunens < ActiveRecord::Migration
  def change
    create_table :gakunens do |t|
      t.integer :gakunen, null: false
      t.integer :sort_order, null: false
      t.boolean :honka, null: false
      t.timestamps null: false
    end
    add_index :gakunens, :sort_order, unique:true
    add_index :gakunens, [ :honka, :gakunen ]
  end
end


FactoryGirl 関係

spec/factories/gakunen.rb は特定の初期値がないので、中身を空のままにする。

FactoryGirl.define do
  factory :gakunen do
  end
end

Factory は spec/support/create_factories/gakunen_factory.rb で作成する。引数は 1 〜 7 までの文字列(.中で to_i してるだけなので、数値でも可)を受け取り、対応するオブジェクトを作成する。一括して全部作成する gakunen_all_factories も用意する。これらの雛形は昨日の template により、モデルテストの下に書かれているので、そこからコピーしてくればよい(ファイルを作ってしまうと必要ないときに消すのが面倒なのでこのようにしている)。

# @param [Fixnum, String] n オブジェクトを一意に決定する数値(文字も可)
# @return [Gakunen] Gakunen FactoryGirl オブジェクト
def gakunen_factory(nstr)
  n = nstr.to_i
  FG.find_or_create(:gakunen,
                    gakunen: (n - 1) % 5 +1,
                    sort_order: n,
                    honka: n <= 5)
end

# @return [Array<Campus>] Campus FactoryGirl オブジェクトの配列
def gakunen_all_factories
  (1..7).map { |n| gakunen_factory(n) }
end

テストの記述

spec/models/gakunen_spec.rb は前回のヘルパメソッドを用いて以下の様に記述した。昨日のメソッドの使用時の説明になるので、部分ごとに説明する。common validation check 部は nil 不可の属性確認、削除ができないことの確認、一意性制約の確認である。

require 'rails_helper'

RSpec.describe Gakunen, :type => :model do
  context "common validation check" do
    before do
      @target = gakunen_factory 1
    end

    check_presence_validates %i( gakunen sort_order honka )
    check_reject_destroy_validates
    check_unique_validates(%i( sort_order )) { gakunen_factory 2 }
  end

次にクラスメソッド、インスタンスメソッドのテストのために、7 個のデータをすべて作成しておく。

  context 'after all gakunens are registered' do
    before do
      @gakunens = gakunen_all_factories
    end

最初にクラスメソッドをテストする。subject は Gakunen クラスになる。一つのメソッドで 3 つのメソッドのテストが実施されている。

    context 'for Gakunen class' do
      subject { Gakunen }
      it 'should receive honka_gakunens, senkouka_gakunens and all_gakunens' do
        target_method_send_test subject,
          :honka_gakunens, nil, @gakunens[0..4],
          :senkouka_gakunens, nil, @gakunens[5..6],
          :all_gakunens, nil, @gakunens
      end
    end

次にインスタンスメソッドをテストする。subject は @gakunens となり、7 個のインスタンスに対してそれぞれメソッドを実施し、一括で 7 個分のテストを行う。ここでは、:full_name, :short_name, :honka?、:senkouka? の 4 つのメソッドを一括で実施しているため、28 個のテストが実施されている。

    context 'for each Gakunen instances' do
      subject { @gakunens }

      it 'should receive full_name, short_name, honka?, senkouka?' do
        target_array_method_send_test subject,
          :full_name, nil,
            %w( 本科1年 本科2年 本科3年 本科4年 本科5年 専攻科1年 専攻科2年 ),
          :short_name, nil, %w( H1 H2 H3 H4 H5 S1 S2 ),
          :honka?, nil, [ true, true, true, true, true, false, false ],
          :senkouka?, nil, [ false, false, false, false, false, true, true ]
      end

同様に前の学年、次の学年を得るメソッドをテストする。

      it 'should receive pre_gakunen and post_gakunen' do
        target_array_method_send_test subject,
          :pre_gakunen, nil,
            [ nil, @gakunens[0..3], nil, @gakunens[5] ].flatten,
          :post_gakunen, nil,
            [ @gakunens[1..4], nil, @gakunens[6], nil ].flatten
      end

最後に honka_ichinen? から senkouka_ninen? までのテストメソッドをテストする。

      it 'should receive honka_*nen?, senkouka_*nen?' do
        target_array_method_send_test subject,
          :honka_ichinen?, nil,
            [ true, false, false, false, false, false, false ],
          :honka_ninen?, nil,
            [ false, true, false, false, false, false, false ],
          :honka_sannnen?, nil,
            [ false, false, true, false, false, false, false ],
          :honka_yonen?, nil,
            [ false, false, false, true, false, false, false ],
          :honka_gonen?, nil,
            [ false, false, false, false, true, false, false ],
          :senkouka_ichinen?, nil,
            [ false, false, false, false, false, true, false ],
          :senkouka_ninen?, nil,
            [ false, false, false, false, false, false, true ]
      end
    end
  end
end

モデルの実装

app/models/gakunen.rb は以下のようになる。ほとんど以前作った Year と同様であるが、Rails 4.1 から導入された ActiveRecord enum を使ってみた。旧システムでは、honka_xxnen? 系のメソッドは honka と gakunen を使ってテストするメソッドを記述していたが、enum によりメソッドは自動生成される。enum については、別のモデルでより便利な使い方をしているので、その時に別途説明する。

# @!attribute year
#   @return [Fixnum] 学年 (1〜5)
# @!attribute sort_order
#   @return [Fixnum] 並び順 (1〜7、専攻科は6, 7)
# @!attribute honka
#   @return [Boolean] 本科であれば true
class Gakunen < ActiveRecord::Base
  LoadKey = %i( id gakunen sort_order honka )
  validates :gakunen, :sort_order, presence: true
  validates :honka, inclusion: { in: [ true, false ] }
  validates :sort_order, uniqueness: true

  GakunenHash = {
    honka_ichinen: 1, honka_ninen: 2, honka_sannnen: 3, honka_yonen: 4,
    honka_gonen: 5, senkouka_ichinen: 6, senkouka_ninen: 7
  }
  enum sort_order: GakunenHash

  before_destroy :on_destroy_validation_method
  # @return [Boolean] false
  # @note 学年は削除できない
  def on_destroy_validation_method
    errors.add(:base, '学年は削除できません')
    false
  end
  private :on_destroy_validation_method

  # @return [Array<Gakunen>] gakunen が n の学年一覧を得る
  scope :gakunen_value_is, -> v { where arel_table[:gakunen].eq v }
  # @return [Array<Gakunen>] honka が flag の学年一覧を得る
  scope :honka_value_is, -> f { where arel_table[:honka].eq f }
  # @return [Array<Gakunen>] sort_order 順に並んだ学年一覧を得る
  scope :order_sort_order, -> { order arel_table[:sort_order] }

  # @return [Array<Gakunen>] 本科の学年一覧を得る
  def self.honka_gakunens
    honka_value_is(true).order_sort_order
  end

  # @return [Array<Gakunen>] 専攻科の学年一覧を得る
  def self.senkouka_gakunens
    honka_value_is(false).order_sort_order
  end

  # @return [Array<Gakunen>] 全学年一覧を得る
  def self.all_gakunens
    order_sort_order
  end

  # @return [String] '本科x年'または'専攻科x年'を返す
  def full_name
    "#{honka ? '本科' : '専攻科'}#{gakunen}"
  end

  # @return [String] 'Hx'または'Sx'を返す
  def short_name
    "#{honka ? 'H' : 'S'}#{gakunen}"
  end

  # @return [Boolean] 専攻科なら true
  def senkouka?
    !honka
  end

  # @return [Gakunen] 学年と本科フラグから学年を得る
  def self.gakunen_from_gakunen_and_honka(g, h)
    self.gakunen_value_is(g).honka_value_is(h).first
  end

  # @return [Gakunen] 一つ下の学年を得る
  def pre_gakunen
    gakunen == 1 ? nil :
      Gakunen.gakunen_from_gakunen_and_honka(gakunen - 1, honka)
  end

  # @return [Gakunen] 一つ上の学年を得る
  def post_gakunen
    (honka_gonen? || senkouka_ninen?) ? nil :
      Gakunen.gakunen_from_gakunen_and_honka(gakunen + 1, honka)
  end
end

timestamps の null 禁止の対応

Rails 4.2 では timestamps が null 禁止推奨となった。今後は完全に禁止になるとのこと。Rails で作成されたデータは当然これらは入っているので問題がないが、別のデータベースから流し込んだデータはこの限りではない。本システムは古いデータは 2001 年に作成されたものもあり、これらのデータについては null のまま運用していた。今後移行時に問題となるので、旧システムの方で対応しておく。

旧システムの方で、以下の簡易スクリプトを書いた。これを load し、新システムで実装完了するたびに、流し込みデータに関して timeCorrect メソッドを実行し、TimeStamp を初期値に設定する。物によってはデータ量も多いので、%表記もしているので、ちゃんと動いていることが確認できる。

def timeCorrect(c)
  d = DateTime.new(2009, 4, 1)
  cc = c.where(created_at:nil)
  ccn = cc.count
  pre = -1
  cc.each_with_index do |o, i|
    o.created_at = d
    o.save
    n = (i+1)*100/ccn
    unless pre == n
      print "#{i+1}/#{ccn} (#{n}%)\n"
      pre = n
    end
  end
	
  cu = c.where(updated_at:nil)
  cun = cu.count
  cu.each_with_index do |o, i|
    o.updated_at = d
    o.save
    n = (i+1)*100/cun
    unless pre == n
      print "#{i+1}/#{cun} (#{n}%)\n"
      pre = n
    end
  end
  ccn + cun
end

今日はここまで。
written by iHatenaSync