railsでFactory Girlでフィクスチャなテストをしてみる

rubyにおけるテストの方法とか一切知らんので、調べてるうちに面白そうなのを見つけた。のでやってみよう。
フィクスチャという言葉の定義が自分の知ってるのとは違ってた。とにかく、ストレージにデータを保存するときのテスト手法みたいな。
true/falseを返すメソッドとか、値を返すメソッドなら簡単にテストできるが、「こんな構造のデータを保存できる」ってメソッドのテストはストレージ使わないと出来ないので、テストデータ作ったり保存したりと。
で、テストデータを用意するのだけど、それをyamlで書くのが面倒だからrubyのデータ構造で書こう、というのがFactory Girlらしい。
解説してるサイトを見ながらやってみた。ほぼそのまま。最後にリンク張っとくので、そっちを見た方が分かりやすいと思うよ!
色々と省略されてる個所があったので、補完しつつメモ。rails使いには書かなくても分かる事なんだろなー。
メモの中でコピペしまくってるのは、参考リンクの中の一番上のサイトから。
ここまで前振り。長いよ。

前準備

必要なgemをインストール。

% sudo gem install rspec-rails
% sudo gem install thoughtbot-factory_girl --source http://gems.github.com

プロジェクト作成

% rails -d mysql girl
% cd girl

別にmysqlにする必要は無い。使い慣れてるからです。むしろこれしかRDBMS知らないから。

設定

% vi config/environment.rb

30行目付近に、config.gemがどうこうとコメントアウトされてるので、追記。
テスト実行時にFactory Girlがロードされる、だそうで。

config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com"

モデル作成

参考リンクまんまで作成。

% ./script/generate model user name:string
% ./script/generate model page title:string url:string
% ./script/generate model bookmark user_id:integer page_id:integer
% vi app/models/*

app/models以下のファイルの編集内容は参考リンク参照。

rspecの雛型作成

% ./script/generate rspec

何か色々できる。

Factory Girlの設定

ここでテストデータの用意。

% vi ./spec/factories.rb

やっぱり参考リンクのコピペで。ruby分からなくても、見れば意味は分かる。

テストコード作成

% mkdir spec/models
% vi ./spec/models/user_spec.rb

はいはいコピペコピペ。
テストメソッド名が it とおもしろい。
るびまの方のサイトで解説してるけど、これはテストの内容とかタイトルとかを英語で書くと、テストの実行結果が分かりやすく出てくる。
日本語で書くこともできるが、英語前提なので、ちと無理やりになる。
テストコードは普通の英文のように書けて分かりやすく、外部の人が見ても意味が分かる。素敵。
こういうのDSLと言うんだっけ。

DBの用意

% rake db:create RAILS_ENV='test'
% rake db:migrate RAILS_ENV='test'

RAILS_ENVを付けないと、developmentで作っちゃうので注意。
テストは当然test用のDBでするので、test用で作らないとテスト出来ません。

テスト実行

% ./script/spec -fs -c spec/models

やっとテスト実行。慣れてればさくっと出来るのだろうけど、調べながらやってたので時間食った。
成功したテストは緑で、失敗は赤で表示される。一般的なxUnitと同じである。
specの引数は

% ./script/spec -h

で。

クエリ確認

DBを見ても、テストに使ったデータは無い。実行したクエリはlogディレクトリ以下に、test.logとして残ってるはず。
見ると、トランザクション処理をやって、テストが終わったらロールバックしてるのが見て取れる。
自動で後始末してくれる。

感想

確かに、多対多(many_to_many)のテストなのに関係テーブルのデータを用意しなくて済むのが素晴らしい。
データの用意だけならyamlでも別にええやんって思ってたけど、これはでかい。記述量も減るし、yamlより分かりやすいし。
あーあとDB無関係なテスト調べてない。普通にTest::Unitでいいのかな。でもspec使えるっぽい。あとで調べる。




ところで。
rails使えないのにrailsのテスト手法調べるとか、TDDを言い訳にする事すら出来ない阿呆っぷり。

スズメが鳴く時間

神とskypesinatraだのvimだのARパターンだのARだの分散だのについて話してた。
会話の一部を無断転載。

俺: ああ、スズメが鳴き始めたけど大丈夫ですか社会人の方
神: 大丈夫じゃないです

俺は有給なので、組織に伝染らないうちにひねり潰されたりすることはないだろう。
あ、神がひねり潰されたらごめん。でも大丈夫だよねーだってこんなのいつm(自重

神宛てに追記

寝坊させてごめん!でも頑張ってとしか言えない!

買い物してきた

土曜にいつものメンツでいつもの街へ。ルートだけちょいと変えたけど行く先は変わらず。
1時間早く集合時間を設定してしまったので最初はどこも開いてなくて散歩状態。人が居ない竹下通りとか初めて見た。これだけで随分寂れて見えたもんだ。


話し掛けてくる店員は適当にあしらってるけど、しつこい店員の場合は、余程気に入った服が無い限りさくっと去る。あれ確実に売上落としてると思うんだがどーか。
話して楽しい店員って滅多に居ないしな。


相変わらず皆買う買う。
「迷ってるなら買ってから考えたらいいよ」
「今買わなくてもどーせ来週買いに来ることになるよ」
「買わずに後悔するより買ってから反省しなさい」
飛び交う会話がおかしい。いつもどおりである。
教訓:馬鹿に財力を与えてはいけません
学生のときの時間と金と、今のそれを足して割りたい。偏りすぎでしょー。


新しい店へ。M氏の知り合いがやってる店。つか、知り合いが原宿で店やってるって凄いな。
小さいけど落ち着いた良い店だった。ここでも一目で気に入ったのを購入。


セルフィユへ。プリンジャムはやめて、フラットブレッドとベジハーブディップを購入。
ここは買いたい物が多すぎて目移りする。甘い物から美味いもの、普段目にしない物まで色々。こういう店が近場にあったら散財してるな。


10時間ほどぶらついたところで飯。今回はT氏の奢りでモツ鍋。ゴチになりました。そして騒ぎすぎである。
「何に乾杯する?」
「んじゃこいつの転職と失敗に」
「よしそれで」
「かんぱーい!」
「ちょっと失敗するって決まtt」
「どーせするって!」
「それもそーだ!」
そんなお前らが大好きです。


ところでこいつらときたら。
T氏の場合
「重大発表があります」
「何?仕事辞めんの?」

M氏の場合
「仕事辞めます」
「実家かえんの?」

そんなお前らが大好きです。
なお一般的な反応の人は省略。


それと宣言どおり次の奢り役は俺!寿司でも焼肉でも何でも可!
お前らは俺には奢るくせに俺が奢ろうとすると受け付けないのをやめるように。


つーわけで楽しい買い物でした。買い物しなくても集まれば楽しい。
やっぱこーいうのしないとねえ。酒飲みが酒のために働くっていうのに近いのだろか。
社会人になってからは集まりづらくなったけど、またどーせそのうち何かやるからヨロシク。

sinatraアプリなどをapache/passenger/mod_railsで動かす

passengerをインストールして、使うためのコマンド実行。

% sudo gem install passenger
% sudo passenger-install-apache2-module

最後にこんなメッセージ。

Please edit your Apache configuration file, and add these lines:

   LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-2.2.3/ext/apache2/mod_passenger.so
   PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-2.2.3
   PassengerRuby /usr/bin/ruby

表示されるパスは環境により異なるはず。まぁ、言われる通りに上記をapacheのconfにコピペ。


あとはアプリケーションルートに public tmp のディレクトリを作成、 config.ru を以下の内容で作成。

require 'start' # sinatraアプリの名前を指定
run Sinatra::Application

あとはapacheのconfに以下を追記。

<VirtualHost *:80>
    DocumentRoot "/path/to/sinatra_app/public"
</VirtualHost>

あとはapache再起動して終わり。
簡単過ぎて泣ける。 perl/apache/mod_perl/catalyst のときはあんなに苦労して調べたのに。


まあこれで終わりでもいいのだけど、やっぱり静的コンテンツ/動的コンテンツでapacheは分けるべきかと。
アクセスログ見たら、cssもjpgもsinatraアプリも全部同じapacheプロセス(mod_railsをロードした、メモリ食いな)が処理してたし。apacheプロセス1個だけだから当然だけど。
passengerは自動でいい感じに処理してくれるとかだったらごめん。確かtomcatだかstrutsは、何も意識しなくても処理してくれるらしいから、passengerもそうなってますよとか。
とりあえず、catalystのときとは異なり、本当にmod_railsをロードしているかいないかの違いしかないconfを2つ用意していい感じに出来た。多分、無駄があるけど、mod_railsに静的コンテンツを処理させるよりは遥かにマシなはず。
静的だの動的だの何それおいしいのって人は つ http://d.hatena.ne.jp/foosin/20090502/1241274828


あと、アプリを再起動したい場合は、apacheを再起動するまでも無く、 tmp ディレクトリ以下に restart.txt を置けばいい。その次のリクエスト時に自動で再起動してくれる。
再起動後、restart.txtは削除されるらしいのだけど、うちでは何度試してもされなかった。apacheが読み書きできる権限なのに。ただ、タイムスタンプを見てるっぽく、最初の1回しかアプリは再起動しない。touchなどすればまた再起動される。
開発中で毎回再起動したいーっていう場合は、 restart.txt の代わりに always_restart.txt というファイルを置けばよろしい。

参考リンク

Sinatraで鼻歌まじりのWeb開発
http://www.modrails.com/documentation/Users%20guide%20Nginx.html (特に 7.6. Making the application restart after each request の項)

rubyでMarshalとBase64を使ってシリアライズ

rubyのデータ構造をDBに保存しようの巻。
シリアライズにMarshalを、DBへの保存の際にBase64を使用。
他にシリアライズの形式としてYAML形式があるけど、Marshalより遅い気がする。多分。
Marshalがデータをバイナリで扱うのに対して、YAMLは平文なので。


セッションデータのDBに保存・リストアの例。

DBのスキーマ

  • session_id
    • セッションを一意に特定するID
  • data
    • データの実体

シリアライズ

require 'base64'

session = {:hoge => 'huga'} # 何かデータを突っ込む
session = Base64.b64encode( Marshal.dump(session) )
Session.new(:session_id => session_id, :data => session).save

シリアライズ

require 'base64'

session = Session.find_by_session_id( session_id )
data = session.data
data = Marshal.load( Base64.decode64(data) )
hoge = data[:hoge]

sinatraとActiveRecordとERBでBBS作ったのでソースを公開してみる

rubyのwafでsinatraが最近人気なのでBBS作ってソース晒してみた。
http://github.com/hirafoo/sinatra_bbs/tree/master
rubyrailsも、ほぼ知識無しの状態でやったので色々見苦しいはず。俺が使えるのはrailsの中でのマイグレーションのみです。


sinatraについては以下が大変参考になります。
http://labs.unoh.net/2009/05/sinatra.html
第9回 SinatraとSequel・Hamlで掲示板アプリを作る:Ruby Freaks Lounge|gihyo.jp … 技術評論社


作ったBBSの機能、特徴など。


にげっとのソースを晒したときは、自分なりのノウハウやらテクいことを詰め込んだつもりなのだけど、全然つっこみが無くて寂しかったので軽く解説してみる。

railsライクなソースの配置

sinatraは非常に軽量なwafで、.rb一つで動かす事が出来る。公式のトップにあるコードのみで動くので参照されたし(実はsinatra0.9.2の時点でsinatra自体にバグがあるのでこのままでは動かない。下の方に対策を書いてあります)ここでは start.rb とする。
http://www.sinatrarb.com/
このstart.rbにMVCの全てを詰め込む事もできるが、やはりMVCのそれぞれでファイルを分けたい。
sinatraではMVC的なファイルの配置は、ビュー以外は用意されていない。アプリケーションルート(以後 $APP_ROOTと表記)にソースを置く事ができ、ビューだけは $APP_ROOT/views が標準で使われる。
モデルやコントローラを同じディレクトリに置きたくないので、railsと同じ配置にしてみる。


$APP_ROOT/app 以下に controllers models views のディレクトリを作成。
start.rbに以下を記述。

set :views, File.dirname(__FILE__) + '/app/views'

( Dir::glob("app/controllers/*.rb") ).each do |controller|
  load controller
end

( Dir::glob("app/models/*.rb") ).each do |model|
  require model
end

モデルとコントローラはそれぞれ1階層しかファイルを探索しないが、個人的にモデルとコントローラは1階層でいいと思うので。俺の経験が浅いからかもだけど。
ともかく、こうすることで start.rb にはアプリ全体の設定などのみを記述して、MVCはそれぞれのファイルに書くことが出来る。
ビューのディレクトリ指定方法はsinatraのソース読んだ。ドキュメントには指定方法載ってなかったと思う。有ったら俺の阿呆。

ORマッパにActiveRecordを使用

sinatraはいわゆるMVCフレームワークではない。持つのはuriディスパッチャ的なもののみ。
モデルとビューは自分で用意してやる必要がある。(余談だけどcatalystもそうだよね)
ネットで見るsinatraの紹介ではモデルにはSequelを使用する例が多いが、ARを使ってみた。
ARをrailsの外で、つまりAR単体で使うのは割と簡単。

require 'rubygems'
require 'activerecord'

で済む。
ARを選んだのは単純に興味があったからなのだけど、後でやっぱりARにして良かったと思う事になる。

railsと同じコマンドでマイグレーション

一度あのマイグレーションを使ったら、もうそれ以外でスキーマ管理する気になれないので。
migrateはググったら出てきた。rollbackはフィーリングで。
Rakefileを作成。

require 'config/boot'
require 'db/migration_helper'

namespace :db do
  desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"

  task :migrate do
    ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
  end

  task :rollback do
    ActiveRecord::Migrator.rollback("db/migrate/")
  end
end

rake db:migrate と rake db:rollback のみしか使えず、STEPの指定なども出来ないが、正直これだけで俺は十分。
というか分からないのでやってない。create/dropも分からない。修正希望点。

マイグレーションの拡張

http://d.hatena.ne.jp/foosin/20090531/1243765471 と同じ。
今回は db/ 以下に配置。そして Rakefile でrequireするだけ。
本当に皆afterや外部キー制約要らないのか?

セッションをデータベースに保存

ここが一番悩んだ。
sinatraはクッキーベースのセッションしか対応していない。
正確には、対応しているらしいが、ドキュメントが白紙である。
http://www.sinatrarb.com/book.html#database_based_sessions
クライアントにセッションの内容まで持たせるのはセキュリティ的に考えられないので、セッションの内容はデータベースに保存、クッキーにはセッションIDのみを保存するようにしたい。
そこで、クッキーからはクッキーが持つ、自身を一意に特定するIDを取得し、そのIDとセッションに持たせたいデータを紐付けて、データベースに保存する。ユーザがアクセスした際は、クッキーからIDを取得し、データベースにそのIDで問い合わせを行い、セッションの内容を取得する…という仕様にした。
セッションの内容をデータベースに保存するには、データのシリアライズが必要である。
ここでARを使ってよかった点その1だ。ARはデータのシリアライズをする機能を備えている。
使い方も簡単。

class Session < ActiveRecord::Base
  serialize :data
end

これで、sessionsテーブルのdataカラムには、自動でシリアライズされたデータが保存される。もちろん取得時も自動でデシリアライズしてくれる。
データはYAML形式でシリアライズされるだけなので、平文で入るのがちょっと残念ではあるが、手動でやるより非常に楽。
もし平文で入るのが嫌なら手動でYAML形式にシリアライズ後、Base64なりAESなり使えばいいかと。
あと、クッキーから取得したIDがとても長かったのでSHA1でハッシュ化した。


余談だが、perlのwafのcatalystにも、同じことをするモジュールがある。
http://search.cpan.org/perldoc?Catalyst::Plugin::Session::Store::DBIC
こちらはデータをStorableというモジュールを使い独自の形式に変換後、Base64をかけている。

認証

「このページはログインしていないと見れない」という処理はよくある。
普通はその処理の直前や特定のクラスにだけ、ログインを確認する処理を挟むが、sinatraにはそんな機能は無い。むしろuri的なクラスの概念が無い。
一応フィルタはあるが、「全ての処理の前に行う」というフィルタしかない。
仕方が無いので、そこでセッションを確認してその結果を変数に持たせ、ログインが必要な処理の直前でその変数を確認する、という方法を取った。
具体的には、まずセッションを確認するフィルタを作成。

before do
  if request.cookies['rack.session']
    session_id = Digest::SHA1.hexdigest( request.cookies['rack.session'] )
    if @session = Session.find_by_session_id( session_id )
      @login_user = @session.data[:user]
    end
  end
end

で、ログインの確認。

get '/entry/' do
  if @session
    erb :'entry/index'
  else
    myerror "please login"
  end
end

ここでは、@sessionにセッションの有無を保存。/entry/にアクセスされた際、@sessionに値が入っている、すなわちセッションが有効ならばページを表示、そうでなければエラーとしている。


クッキーからIDを取得する方法に自信が無い。多分、別のRackを使ったアプリと混ざったら残念なことになるかもしれない。

ページング

さすがに手動でページングするのは厳しいので、railsにおける標準的なページングモジュールであるwill_paginateを使用する。
これはARに対応している。ARを使って良かった点その2だ。
だが、これをrailsの外で使う方法が分からなかった。各モジュールをrequireすればいいのかと思ったのだけど、それだとモデルにpaginateメソッドが生えない。
#rails-tokyo@freenodeで質問して教えてもらった。答えてくれたmorohashiさんに感謝。
start.rbに以下を記述。

require 'will_paginate'
require 'will_paginate/core_ext'
require 'will_paginate/view_helpers'

WillPaginate.enable_activerecord

つまり WillPaginate.enable_activerecord が足りなかった。ちなみにこれ、will_paginateのソースの先頭にそれっぽいのが書いてある。
分からなければソース読め、ということだ。反省。


これでデータを取得する際の処理は出来たのだが、ビューの中でページャリンクを作成するためにwill_paginateを使う方法は分からなかった。
仕方ないので非常にお粗末なリンクを置いといた。修正希望点。

データベース接続情報の外部ファイル化

プログラム中に定義してもいい情報と、どう考えてもすべきでない情報というものがある。
DBへのコネクション情報は明らかに後者。
アプリの起動時と、マイグレーション時に使用する。
railsと同じ配置で、コネクション情報を書いたyamlを用意する。

db_config = YAML::load_file('config/database.yml')
ActiveRecord::Base.establish_connection(db_config)

ビューでフィルタを使用する

以下参照。
http://www.sinatrarb.com/faq.html#escape_html
こう書けるわけだ。

<%=h hoge %>

変数hogeに、htmlの仕様上エスケープすべき文字があれば、エスケープしてくれる。

sinatraWEBrickで起動しようとしたらエラーが起こる

以下参照。
http://d.hatena.ne.jp/unageanu/20090525/1243268383

ソースを編集した後、WEBrickを再起動するのが面倒

以下参照。
http://d.hatena.ne.jp/foosin/20090611/1244735821


だいたいこんなところですか。
rubyの知識ほぼ無し、ましてやrubyでアプリ作ったのも初めてなので色々残念なところがあると思うので、突っ込んでもらえると喜びます。
以下、todo的なもの。

分かる人居たら教えてもらえるとありがたいです。




ところで。
作ってる最中に自分でちゃんと感づいてはおりました。


それなんてrailsアプリ?


軽量が売りのはずのsinatraが随分ゴテゴテしてしまったという。
勉強になったのでよしとする。

sinatra0.9.2でファイルの自動再読み込みが効かないのでshotgunを使おう

WEBrickが起動しなくなったりしてアレなsinatra。でももっと痛いのがファイルを編集しても自動で再読み込みされなくなったこと。
catalystrailsで言えば、ファイル編集する度に毎回server.plやserverを手動で停止>実行してるに等しい。発狂するわ。


CHANGESにちゃんと書いてた。

= 0.9.2 / unreleased

* Development mode source file reloading has been removed. The
"shotgun" (http://rtomayko.github.com/shotgun/) program can be
used to achieve the same basic functionality in most situations.
Passenger users should use the "tmp/always_restart.txt"
file (http://tinyurl.com/c67o4h). [#166]

というわけで

% gem sources -a http://gems.github.com
% sudo gem install shotgun
% shotgun start.rb -p 3000

で無事に自動再読み込みしてくれた。shotgunのインストールが異常に重くて3回やって成功した。
Passengerは使ってないのでalways_restart.txtは作らなくてもいい。