第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:記憶を便りに書いているので会場でやったときとは例題も違う点に注意

標準の日付の表示形式を設定する

色々落ち着いてるので久々にRailsを触る

標準の表示だと英語圏に沿った表示になってるので変更したいなと探してみたら案の定あった。

# environments.rb
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!({
  :default => "%Y年%m月%d日 %H:%M",
})

あと日付選択も月名が英語になっているのを毎回設定するのはめんどいのでこうした。application_helper.rbでやるよりもconfig/environments.rbでやる方がふつうなのかな。

# app/helpers/application_helper.rb
module ApplicationHelper
  include ::ActionView::Helpers::DateHelper

  alias default_date_select date_select
 
  def date_select object_name, method, options = {}
    options[:use_month_numbers] ||= true
    default_date_select(object_name, method, options)
  end 

  alias default_datetime_select datetime_select

  def datetime_select object_name, method, options = {}
    options[:use_month_numbers] ||= true
    default_datetime_select(object_name, method, options)
  end 
end

Ruby忘年会

Rails勉強会の後はRuby忘年会に参加。

一次会

50名参加ということで1テーブルあたり約10人とやや狭かった。自分たちのテーブルは一番奥ということもあってかやや広めだった。

一次会は自分のテーブルはささださんとコーディングスタイルの話で少し盛り上がった。

前にささださんに会ったときはキモいといって否定していたメソッド定義のときの括弧の省略は最近自分も使うようになった。

呼び出しのときの括弧の省略は結構ややこしくて後から変更すると括弧をつけなきゃいけなくなったりして最近はあきらめて最初から書くことも多いのだけど、メソッド定義時であればだいたいのケースで大丈夫なので括弧は省略してしまう。

また、レシーバが省略できるメソッドで引数のない呼び出しの場合()をつけるようにしている。これはローカル変数とメソッドかぱっと見たときに判断がつかないからなのだけど、selfをつけるのが一番見栄えが良いのだけどprivateなメソッドの場合selfをつけることができなくなったので()をつけるということにしている。

 def hoge a, b, c=nil  # 括弧を付けなくても問題なし
    # ...
 end
 
 hoge "foo", "bar"  # pでデバッグするときなどでどうせ括弧を付けなければいけなくなる
 
 def foo
   # ...
 end
 
 foo = "fuga"
 foo()  # レシーバがなくて引数のないメソッドはローカル変数と見分けがつけにくいときがある

それから、ささださんは捨て変数としてよく_を使うのだそうだ。さすがにこれには同感できなかった。

 [1, 2, 3].each {|_| p _ }

二次会

二次会は20名ちょいくらいだったと思う。

高橋さんによればPHPUnitはメソッド名に日本語が使えるのが良いのだそうだ。高橋さん的にはRSpecもshould_beとかではなくて「は」などのメソッドにしてほしいらしい。

$KCODEをUTF-8にすれば日本語が使えるとかなんとかを角谷さんがテストしていたが結果どうなったかは少し覚えていない。リテラルの扱いを変更する際はコードで$KCODEを変更するのではなく、オプションで変更しなきゃいけないというのが少しめんどくさい。

最後の方でid:ogijunさんが駆けつけた。[あとで払う]にみんな爆笑していた。

来年はもうちょっとRubyでライブラリ作ったりアプリ書いたりして参加しないとなぁと思った。

Rails勉強会@東京第13回

忘年会で最近更新があまりないと言われてしまったのでちゃんとログぐらいは書くことにします。

Rails勉強会@東京第13回に参加してきました。

AWDwR 2ndを読む

前半はかわむらさんのAgile Web Development with Rails 2nd Editionをみんなで読みながら具体的にはどの辺りが変わったのかを比べたりしました。記憶を便りに比較したので必ずしも正しくない点はご了承ください。

  • まだ出ていないRails v1.2に準拠
  • Depotの作成ではRJSテンプレートを利用したAjaxによるカートの作成が加わっているようでした。
  • ActiveRecoardのRelationのところがだいぶ書き変わっている
    • habtmは一応載っている
    • has_many :throughとhabtmの違いは外部キー以外にカラムを持つかどうかで分けているのではないか
  • ActionPackがActiveControllerとActionViewの二つの章に分けられた
    • ActiveController
      • RESTの説明にやたらとページが割かれている

とかが気になった点でした。

RSpec

前回参加してなかったし、セッションがあまり埋まってなかったのでid:moroさんに頼んでRSpecをもう一度やってもらいました。

RSpecとTest::Unitの何が違うのか?順番が入れ替わっただけじゃないか」という人もいるのだけれど、自分的にはそれだけで全然読みやすさも書きやすさも違うのでRSpecを使う理由になると思う。それ以外のメリットとしては、ContextごとにSpecificationをリストできるというところ。でもこれは自分ののところだと何故か出力できてなかったりする。Test::Unitだとリテラルであるところが文字列なので日本語を書けるという点もメソッド名に悩まなくなったりしてメリットなのだそうだ。

ちなみにRSpecは0.7から大幅に仕様が変わってそれ以前のものは0.7以降では動かなくなっていたりする。ということで個人的にはRSpecのファイルには

require 'rubygems'
require_gem 'rspec', '> 0.7'

を他の人も入れてほしいのだけど、角谷さんによるとtrunkはまだまだ仕様変更しそうな感じであるしRSpecを使う利点は専用のコマンドがあることでテストに対するいらない障壁がなくなることにもあるので余計なものは書かないという方針らしい。

RSpecを使う難点と言えば、ライブラリを使っていた際にTest::Unitであれば色々な拡張されたAssersionがライブラリで提供されている場合もあるのだけど、RSpecのメソッドまで用意されていることは少ないというところがある。RSpecの知名度が上がっていけば解決される問題なのかなと思ったり。

gettext-scaffoldのパッチ

初期状態ではshow、confirm_create、confirm_update、confirm_deleteのページのカラムがローカル化するようにされてないのでそれをローカル化するパッチを書きました。(もしかするとローカル化するようにされていないのは何か罠があるからかもしれませんが。

# === gettext_scaffold_generator.rb
# ==================================================================
# --- gettext_scaffold_generator.rb (revision 109)
# +++ gettext_scaffold_generator.rb (local)
# @@ -23,7 +23,7 @@
 elsif column_type == :date or column_type == :datetime
   column_modifier = '.to_s(:long)'
 end
 - "<dt><%=h '#{column.human_name}'%></dt><dd><%=h(@#{record}.#{column.name}#{column_modifier})#{column_escaped_modifier} %></dd>" }
 + "<dt><%=h _('#{record.classify}|#{column.human_name}') %></dt><dd><%=h(@#{record}.#{column.name}#{column_modifier})#{column_escaped_modifier} %></dd>" }
 end
 end

# === templates/view_list.rhtml
# ==================================================================
# --- templates/view_list.rhtml (revision 109)
# +++ templates/view_list.rhtml (local)
# @@ -23,4 +23,4 @@
 <%%= link_to _('Next page'), { :page => @<%= singular_name %>_pages.current.next } if @<%= singular_name %>_pages.current.next %>
 <br />
 -<%%= link_to _('New %{name}') % {:name => '<%= singular_name%>'}, :action => 'new<%= suffix %>' %>
 +<%%= link_to _('New %{name}') % {:name => _('<%= singular_name%>') }, :action => 'new<%= suffix %>' %>

rdefsをRails仕様に拡張してみる

最近はRubygemsRSSを眺めて面白そうだなと思ったらインストールして軽く使い方とソースを眺めるというのがマイブームだったりするのですが、ソースを眺めるときに青木さんのrdefsは便利だなぁと思いました。

ただRailsとかだとcattr_readerとか色々拡張されているのでそれっぽいのをActiveSupportから調べてみました。(既に誰かやってそうな気もするけど。

--- bin/rdefs_orig      2006-09-12 23:29:24.000000000 +0900
+++ bin/rdefs   2006-09-10 16:55:17.000000000 +0900
@@ -7,10 +7,25 @@
 DEF_RE = /\A\s*(?:
     def\s | class\s | module\s | include[\s\(]
   | alias(?:_\w+)?
+  | alias_method(?:_\w+)?
   | attr_reader[\s\(]
   | attr_writer[\s\(]
   | attr_accessor[\s\(]
   | attr[\s\(]
+  | cattr_reader[\s\(]
+  | cattr_writer[\s\(]
+  | cattr_accessor[\s\(]
+  | mattr_reader[\s\(]
+  | mattr_writer[\s\(]
+  | mattr_accessor[\s\(]
+  | class_inheritable_reader[\s\(]
+  | class_inheritable_writer[\s\(]
+  | class_inheritable_array_writer[\s\(]
+  | class_inheritable_hash_writer[\s\(]
+  | class_inheritable_accessor[\s\(]
+  | class_inheritable_array[\s\(]
+  | class_inheritable_hash[\s\(]
+  | reset_inheritable_attributes
   | public[\s\(]
   | private[\s\(]
   | protected[\s\(]

何か抜けとか他にもこういうのも加えとくと良さそうとかあればコメントとかください。

cattr_readerなどは同様のことをしたいときのデフォルトになり得る気もするので、ActiveSupportの中身を再解体してCPANっぽくclass-attribute_accessorsとかclass-inheritable_attributesとかに分けて欲しいなと思いました。

さすがに

 require 'rubygems'
 require 'active_support/core_ext/class/attribute_accessors'

はめんどいと思ってしまう。(と思いつつ今打ってみたら案外めんどくさくなかった

まぁRuby使ってる人はたいていActiveSuportをインストールしてるのでActiveSupportに依存した方が可搬性は高いと思いますが。