type_castに気がつかなかった。User.new(:school_id => @school)はだめなのね

とんでもない見落としをしていた。こんなテストコード書いてた。

  fixtures :pay_types
  
  def test_record
    o = Order.new(:pay_type_id => pay_types(:one))
    assert_equal o.pay_type, pay_types(:one)
    ...
  end


このテストは通りますが、やってはいけない書き方です。何がいけないかというと、「:pay_type_id => pay_types(:one)」の部分。
以下の例をみるとおそろしさが分かる。

Loading development environment.
>> o = Order.new(:pay_type_id => PayType.find(2))
=> #<Order:0x22f8080 @new_record=true, @attributes={"name"=>nil, "pay_type_id"=>#<PayType:0x22f8008 @attributes={"updated_at"=>"2008-01-12 12:20:07", "id"=>"2", "pay_type"=>"cc"}>, "address"=>nil, "email"=>nil}>
>> o.pay_type_id
=> 1


「pay_type_id => PayType.find(2)」ってやったから、この結果は2になると思ってました。

type_castで変換される

何故直前の例の最後が2にならないかと言うと、キャストに関係があります。「o.pay_type_id」のように、カラムのデータへのアクセサを呼ぶと、内部的には「ActiveRecord::Base#read_attribute」メソッドが使われ、その中でキャストが行われます。
キャストはこんな感じ。

 # Casts value (which is a String) to an appropriate instance.
      def type_cast(value)
        return nil if value.nil?
        case type
          when :string    then value
          when :text      then value
          when :integer   then value.to_i rescue value ? 1 : 0
          when :float     then value.to_f
          when :decimal   then self.class.value_to_decimal(value)
          when :datetime  then self.class.string_to_time(value)
          when :timestamp then self.class.string_to_time(value)
          when :time      then self.class.string_to_dummy_time(value)
          when :date      then self.class.string_to_date(value)
          when :binary    then self.class.binary_to_string(value)
          when :boolean   then self.class.value_to_boolean(value)
          else value
        end
      end


注目は「when :integer then value.to_i rescue value ? 1 : 0」の部分。pay_type_idの型はinteger(MySQLで)です。よって、キャストされる際(先程のテストの例で言えば、o.pay_type_idの際にキャストを受ける)には「to_i」の変換を受けます。しかし、value(この例では、PayTypeオブジェクト)は「to_i」を持たないので、例外が発生し、結果、1が格納されます。

先程のテストの改善

そもそも、先程のテストが何故通るかというと、「pay_types(:one).id」の結果がたまたま1だったからです。しかし、この1は「pay_types(:one).id」の1ではなく、キャストで発生した例外の結果でした。
実際は、次の様に書きます。

  o = Order.new(:pay_type_id => pay_types(:one).id)


次のようにも書けます(あまりおすすめはしない)。

  o = Order.new(:pay_type => pay_types(:one))


これは、内部では「o.pay_type = pay_types(:one)」と処理されるので、結果は上の書き方と同じになります。


まぁ、実際のコードでは「o = Order.new(:pay_type_id => pay_types(:one))」のように、オブジェクトをHashの形で指定することはないと思うのですが(Order.new(params[:order])とかが多いかと)、テストコードや、または、PayType(支払い方法)のように、決められた値だけを持つデータベースを使う場合は、このような書き方をしてしまうかもしれません。