第16回Rails勉強会

http://wiki.fdiary.net/rails/?RailsMeetingTokyo-0016

時間をおくと先月のように書かなくなってしまいそうなのでとりあえず書く。

今回はやや人が少なめだった。

RSpecについて

RSpecを使うとTest/Unitを使うことよりどんなことがうれしいかということの再確認をした。

個人的には

  • とりあえずテストに入るまでの書く量が少なくて済む(専用コマンドがあるため)
  • 何をテストするのかのリストを出力できる。
  • 標準で色がつく
  • expect, actualの順に書かなくても良い。(エラーメッセージを気にしなければ別に逆でも問題ないよねと舞波氏
  • テストの切り分けがクラス単位意外でもしやすい
  • context_setupとcontext_teardownがある。
  • Mock機能がついている(test/unitでもFlexMockとかMochaを併用すればOK

これぐらいかなと思う。

今までは,

@stack.should_empty

のように書いてきたのだが,最新のバージョンだとobsolatedになるそうな。

@stack.should be(:empty)

と書くようになるらしい。個人的にはエディタの動的略称展開が使えるので前の方が良かった。

その代わり,trunkだとspecifyの引数の文字列を省略しておくと,ブロック内のコードからメッセージを自動生成してくれる機能がついているらしい。これは便利かなと思った。

ActiveRecordを雰囲気Drivenから脱出する

ActiveRecordのよく分からないところを実際に試して検証してみようというセッション。

主に関連づけられたモデルはいつDBに保存されるのかということについて検証してみた。

会場では./script/runnerを使って検証した。何度もデータが同じ状況で実験するためにはテストとして書いてしまった方が良いとのことなので,このログはテストコードでまとめる。*1

まずサンプルのモデルの構造から

class CreateShops < ActiveRecord::Migration
  def self.up
    create_table :shops do |t|
      t.column :name, :string, :null => false
    end
  end

  def self.down
    drop_table :shops
  end
end
class CreateServings < ActiveRecord::Migration
  def self.up
    create_table :servings do |t|
      t.column :shop_id, :interger, :null => false
      t.column :menu_id, :interger, :null => false
      t.column :created_at, :datetime, :null => false
      t.column :updated_at, :datetime, :null => false
    end
  end

  def self.down
    drop_table :servings
  end
end
class CreateMenus < ActiveRecord::Migration
  def self.up
    create_table :menus do |t|
      t.column :name, :string, :null => false
      t.column :price, :integer, :null => false
    end
  end

  def self.down
    drop_table :menus
  end
end

次にモデルで関連付けの設定をする

class Menu < ActiveRecord::Base
  has_many :shops, :through => :servings
  has_many :servings
end
class Serving < ActiveRecord::Base
  belongs_to :shop
  belongs_to :menu
end
class Shop < ActiveRecord::Base
  has_many :menus, :through => :servings
  has_many :servings
end

適当にfixtureを書く

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
katsudon:
  id: 1
  name: カツ丼
  price: 500
tendon:
  id: 2
  name: 天丼
  price: 600
chukadon:
  id: 3
  name: 中華丼
  price: 450
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
one:
  id: 1
  shop_id: 1
  menu_id: 1
two:
  id: 2
  shop_id: 1
  menu_id: 1
three:
  id: 3
  shop_id: 1
  menu_id: 1
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
washoku:
  id: 1
  name: 和食屋
youshoku:
  id: 2
  name: 洋食屋

ShopにMenuを追加してみる

require File.dirname(__FILE__) + '/../test_helper'

class ShopTest < Test::Unit::TestCase
  fixtures :shops

  # Replace this with your real tests.
  def setup
    @shop = Shop.find(2)
  end

  def test_add_menu_failed
    assert_raise ActiveRecord::HasManyThroughCantAssociateNewRecords do
      menu = Menu.new :name => "シチュー", :price => 500
      @shop.menus << menu # servingsが存在していないため例外発生
    end
  end

  def test_add_menu_success
    menu = Menu.new :name => "シチュー", :price => 500
    menu.save!
    @shop.menus << menu  # @shop.updateをかけなくてもservingsは保存される
    assert(Menu.find_by_name("シチュー"))
    assert(Serving.find_by_menu_id(Menu.find_by_name("シチュー").id))
  end

end

Belongs_to側の場合

require File.dirname(__FILE__) + '/../test_helper'

class ServingTest < Test::Unit::TestCase
  fixtures :menus
  fixtures :servings

  # Replace this with your real tests.
  def setup
    @servings = Serving.find(1)
  end

  # セーブする前にupdateをかけてしまうとどうなるか
  def test_add_add_and_update_before_save_menu
    assert_not_nil(@servings.menu)
    @servings.shop = Shop.find_by_name "洋食屋"
    @servings.menu = Menu.new :name => "シチュー", :price => 500 
    assert_nil(Menu.find_by_name("シチュー"))
    @servings.update # 例外は起こらない?
    assert_nil(Menu.find_by_name("シチュー"))
    assert_equal("シチュー", @servings.menu.name) # 新しいデータも残ったまま
    assert_equal("洋食屋", @servings.shop.name) # 新しいデータも残ったまま
    invalid_serving = Serving.find(1)
    assert_equal("カツ丼", invalid_serving.menu.name) # まだ更新されてない
    assert_equal("洋食屋", invalid_serving.shop.name) # Shopは更新される!!
    @servings.menu.save!
    @servings.update
    valid_serving = Serving.find(1)
    assert(Menu.find_by_name("シチュー"))
    assert_equal("カツ丼", valid_serving.menu.name) # 一度updateをかけてしまうとダメ
  end

end

あとはcreateとnewの違いとか,そこら辺を確認した。

ちなみにTransactionが有効かどうかのテストを書くときはテストDBをMySQLInnoDBなどにしなければならない模様。

あとActiveRecordからSQLiteでBlobを保存すると画像が壊れると聞いたのでそれの実験もした。それに関しては,つかもとさんがパッチを作ってくれた模様。GJ!

それにしても今実験するのにRSpecやっぱり使わなかった。。。やっぱり標準でサポートされてることは大きい。

*1:記憶を便りに書いているので会場でやったときとは例題も違う点に注意