model 関連テストの手抜き法

モデル同士の関連のテストは,ほぼ似たようなコードになります.これは手抜きをする価値があるということで,spec_helper.rb に以下のコードを書いています.実際には,昨日の Spork.prefork の中に書いてあります.

Spork.prefork do
  (中略)
  class SpecCreateDeleteCheck
    def initialize(check_class, hash)
      @check_class = check_class
      @check_keys = hash[:check_keys] || []
      @relation_strings = hash[:relation_strings] || []
      @one_relation_strings = hash[:one_relation_strings] || []
      @attributes = hash[:attributes] || {}
      @method_name = ncm(hash[:method_name], '') { |o| o.to_s }
      @one_method_name = ncm(hash[:one_method_name], '') { |o| o.to_s }
    end
    attr_reader :check_class, :check_keys, :relation_strings, :one_relation_strings
    attr_reader :attributes, :method_name, :one_method_name

    def make_object_with_relations(relations)
      an_object = @check_class.create @attributes
      @relation_strings.each do |ww|
        ww.to_s.constantize.instance_method(@method_name).bind(relations[ww]).call << an_object
      end
      @one_relation_strings.each do |ww|
        ww.to_s.constantize.instance_method(@one_method_name + '=').bind(relations[ww]).call(an_object)
      end
      an_object
    end
  end

  def create_delete_check(check_class, hash)
    s = SpecCreateDeleteCheck.new(check_class, hash)
    create_check s
    delete_check s
  end

  def create_check(s)
    context  "作成するとき"  do
      before(:each) do
        @valid_attributes = s.attributes.dup
      end

      it "正しいアトリビュートに対して作成が成功すること" do
        s.check_class.create!(@valid_attributes)
      end

      s.check_keys.each do |key|
        it "#{key} 属性が設定されていない場合にバリデーションに失敗すること" do
          @valid_attributes.delete key
          s.check_class.new(@valid_attributes).should_not be_valid
        end
      end

      s.relation_strings.each do |w|
        it "#{w} に所属するオブジェクトが一つ増えること" do
          r = @relations[w]
          method = w.to_s.constantize.instance_method(s.method_name).bind(r)
          lambda { s.make_object_with_relations(@relations) }.should change(method.call, :count).by(1)
        end
      end

      s.one_relation_strings.each do |w|
        it "#{w} から #{s.one_method_name} として参照できること" do
          r = @relations[w]
          method = w.to_s.constantize.instance_method(s.one_method_name).bind(r)
          child = s.make_object_with_relations(@relations)
          method.call.should == child
        end
      end
    end
  end

  def delete_check(s)
    if s.relation_strings.length > 0 || s.one_relation_strings.length > 0
      context "削除するとき" do
        before(:each) do
          @an_object = s.make_object_with_relations(@relations)
        end

        s.relation_strings.each do |w|
          it "#{ w } に所属するオブジェクトが一つ減ること" do
            method = w.to_s.constantize.instance_method(s.method_name).bind(@relations[w])
            lambda {
              @an_object.destroy
              @an_object.should_not have(1).errors
            }.should change(method.call, :count).by(-1)
          end
        end

        s.one_relation_strings.each do |w|
          it "#{w}.#{s.one_method_name}が nil になること" do
            method = w.to_s.constantize.instance_method(s.one_method_name).bind(@relations[w])
          end
        end
      end
    end
  end
  (以下省略)
end

モデルの spec では,以下のようなコードを書きます.ここでは Syllabus というテーブルを追加した場合を例として記述します.Syllabus は Year と Ujnumber に所属しています(belongs_to).ただし,Year とは 1 対多 (has_many) の関係であり,Ujnumber とは 1 対 1 (has_one) の関係です.

# vim:set fileencoding=utf-8 filetype=ruby:
require 'spec_helper'

describe Syllabus do
  fixtures :syllabuses, :ujnumbers, :years

  before :each do
    @relations = { Ujnumber:ujnumbers(:ujnumber_digital_circuit_II), Year:years(:year_2009) }
  end

  create_delete_check Syllabus,
    attributes:{
      gaiyou:"value for gaiyou",
      hyouka:"value for hyouka",
      kankei:"value for kankei",
      kyoukasho:"value for kyoukasho",
      susumekata:"value for susumekata",
      mokuhyou:"value for mokuhyou"
    },
    check_keys:[ :gaiyou, :hyouka, :kankei, :susumekata, :mokuhyou ],
    relation_strings:[ :Year ], method_name: :syllabuses,
    one_relation_strings:[ :Ujnumber ], one_method_name: :syllabus
end

create_delete_check の第一引数はテストをするクラス,二つの引数はチェック用のハッシュになっています.

attributes
設定する属性値
check_keys
null チェックをする属性名
relation_strings
has_many チェックするクラス名
method_name
has_many のメソッド名
one_relation_strings
has_one チェックするクラス名
one_method_name
has_one のメソッド名

ハッシュなので has_many や has_one がない場合,null チェックしなくてもいい場合は,省略できます.

上記のテストを --format documentation で出力すると以下のようになります.数行書くだけでこれだけのテストを自動実行してくれるので,テーブルを新規に作成することが苦でなくなりました.

Syllabus
  作成するとき
    正しいアトリビュートに対して作成が成功すること
    gaiyou 属性が設定されていない場合にバリデーションに失敗すること
    hyouka 属性が設定されていない場合にバリデーションに失敗すること
    kankei 属性が設定されていない場合にバリデーションに失敗すること
    susumekata 属性が設定されていない場合にバリデーションに失敗すること
    mokuhyou 属性が設定されていない場合にバリデーションに失敗すること
    Year に所属するオブジェクトが一つ増えること
    Ujnumber から syllabus として参照できること
  削除するとき
    Year に所属するオブジェクトが一つ減ること
    Ujnumber.syllabusが nil になること

本当は,親のオブジェクトを削除したときの挙動も入れようかと思ったのですが,dependent によっても異なりますし,元々削除することを想定していないテーブルなどもあるため,こちらは個別に実装しています.

あまり参考にはならないかと思いますが,とりあえず紹介まで.テスト自体もプログラムで書けるということで,いろいろな手抜きができますね.