TDDBC2.1へ参加してきました
2011/09/24に2回目の参加となるTDDBCへ参加してきました。今回も非常に得るものの多い良い会でした。感想などを含め参加して思った事をつらつらと。
TDDに対する悶々とした悩み
TDDをそれなりに大規模で複数人な開発の中で適用しようとすると、上流設計から実際にTDDを始めるまでの間の設計をどこまでするべきか、についてずっと悶々としている部分があった。
これまでのTDD
これまで自分が実務で適用してみたTDDは、詳細設計でクラス設計をしながらユースケースごとのシーケンスを導き、そこで呼ばれる各モジュール単位のI/Fやある程度の内部処理を明確にした上で、それらをインプットとしてモジュールに対するテストを書き、コーディングしていくというスタイルだった。こうした背景はコーディングに入る前に作るべきモジュール数を明確にし進捗をわかりやすくするというのと、できるだけ上流工程での設計レビューを徹底することで設計思想と異なる設計にならないよう一定品質を保ためのチェックを行うためだと思う。確かに詳細設計のレビューでクラスやメソッドの責務やI/Fの認識違いをある程度排除することはできるが、実際にコードを書き始めるとモジュールを実装するのに必要な情報不足で再設計したり責務が不適切でモジュール分割やクラス分割をしたりと詳細設計の段階で決めたことからどんどん変わって行くことが多い。
開発者の足かせ
こうなるとモジュール数での進捗管理は分母がどんどん変わるので意味を成さないし、詳細設計でガチガチに設計してしまっていることがかえってコーディング時の自由度を奪うことにもなっていた。開発者の心境としては、「もっと良い設計が思い浮かんだけど詳細設計を直すのが面倒」「DRYにするためにモジュール分割をやりたいけどモジュール一覧のメンテは面倒だし進捗に見えちゃうからやり難い」などリファクタリングを妨げる足かせとなってしまい、現状の設計を大きく崩さないように無理な責務割当てやその場凌ぎの対応を余儀なくされ、より良いアイデアや設計に軌道修正していくこと自体の妨げとなってしまう。品質が悪い時に上の連中が二言目に言うのは「上流での品質確保」だが、どんなに上流工程で詳細設計をがんばっても実際にコードを書いてみなくてはわからない部分は必ず存在するし、そこで明確になったベストな解をいかに容易に設計へフィードバックしてより良いソフトウェアとしていくかの課題について考えて行かなくちゃいけないと思う。
ユースケース駆動開発
で、上で書いたようなやり方だとTDDのスコープが狭過ぎてそのメリットを活かしきれていないと感じていた。じゃあどこまでの設計をした上で TDD に入ればいいの?について、わたなべさんの発表を聞いていて思ったのは、ユースケースシナリオからロバストネス分析でクラス抽出してソフトウェアを構成する全体クラスを把握した上でユースケースごとのざっくりシーケンス(クラス間のやりとりレベル)を書くところまでやって、あとはアーキテクチャを適用しつつ TDD でユースケースシナリオを実装していくくらいが丁度良いのかなという感触を得た。フレームワークによってはある程度実装パターンが決まっていて各ユースケースごとのシーケンスというよりは代表的なのが一本あれば十分かもしれない。とにかく詳細設計で絵に描いた餅を設計しすぎないようにして、TDDを実践する過程で湧き出る設計アイデアや課題を逐次チームメンバで議論しながら適用していけば良いのかなと。以前読んだアジャイルプラクティスの「設計は指針であって、指図ではない」という言葉が心に響く。
Cucumber の衝撃
演習で Railsチームでやらせてもらった Cucumber を用いた ATDD(Acceptance TDD)はまさにそういう感じで、ユースケースシナリオから直接受け入れ試験を導き出し、Capybaraを用いて動くテストシナリオを作りながらユースケースを実装していった。ある意味今回体験した開発プロセスはテストシナリオから直接 TDD でコードに落とすという究極的な方法で、たぶんもう少し大規模でかつ複雑度の高いソフトウェアの場合は事前にある程度のクラス設計とメインフローの検討やエラー方針の設計などを行い、モデルやコントローラのロジックレベルのユニットテストも必要なんだろうけど、「動くシナリオ」という直接実行可能な受け入れ試験の実装を積み重ねて行くというユースケース駆動開発の考え方は目から鱗だった。もちろんRailsという素晴らしいライブラリが揃っているかなり進んだ開発環境の成せる技というのはあるけれど、他の組み込み系の分野などでも受け入れ試験の観点の試験を自動実行可能なレベルで開発を駆動するというこの考え方は活かせるのではないかと思った。
受け入れテストを進捗に
進捗に関してもユースケースから導きだされたテストケースの通過数とするというのも非常に腑に落ちた。「何モジュールできた」というのはそもそも変数だから進捗の本質ではないし、もし1つのユースケースを実装する内部進捗の為にモジュール数が必要と考えるのであればその時点でユースケースの粒度が大きすぎるのかもしれない。細かくユースケースを積み上げていくことで進捗を表現し、恐らく発生するであろうリファクタリングによる他のユースケースへの影響(デグレ)も jenkinsなどを用いた受け入れテストの自動化で素早く発見・フォローすることで「テストケースの通過数=進捗」として実態とかけ離れた値にはならないはずだ。
現実は
これらを現実でできるかどうかは結局メンバ次第になってしまうかもしれない。経験の浅い開発者やそもそもソフトウェアに興味の無い開発者だったりオフショア先の会ったことも無い開発者にとっては、詳細に書かれた設計書が無いとコーディングなんてできないと言って来る場合もある。TDDで創造性を発揮できるスコープの幅をどう持たせるかは、そういったチームを構成する開発メンバのスキルやバランスを加味して調整していく必要もあるのだろうし、もちろん彼らを変えて行く努力も必要だ。また、開発環境的な問題もありえる。この辺りはどちらかというとプロジェクトリーダの頑張り次第でなんとかなる事も多いけど、ソフトウェア3本柱として言われている「バージョン管理」「テスティング」「自動化」さらに「チケット管理」をどのくらい整備できるかや、チーム内のコミニュケーションや情報共有の発達度も大きく影響してくる。
今後は
まぁ、いろいろと敷居が高い要因はあるけれど、どうせやるのであればTDDのスコープの幅をできるだけ広げて攻める姿勢で開発者一人一人の創造性や創意工夫を活かしつつ楽しく成長できてやりがいのある開発にしたいなと思う。TDDBC2.1ではこれらを実現するためのいろいろなヒントやアプローチを聞けて良かったし、具体的に手を動かす事で得られるたくさんの武器を講師の方々やチームメンバからいただいた。これからの自分のソフトウェア開発との向き合い方に活かして行こう。
will_paginateでAjax対応 Rails3.0系
基本は以下を参考にする。
http://techracho.jp/morimorihoge/2011_06_29/3859
以下、上記だけではハマったことをメモ。
RJSで Element.update が無いというエラー発生
will_pagenate のリンクをクリックすると、ブラウザのダイアログでエラーが出て驚く。
調べてみると、Element は prototype.js で定義されており、jQueryを使っている場合はそれが存在しないので自前で定義してあげる必要がある。 以下を参考に public/javascripts/application.js へ追記する。
// public/javascripts/application.js Element.update = function (element, html) { $('#' + element).html(html); }
IEでは Element が無いというエラー
上記対応だけでは、IE6 でエラーになる。(またIEかよ!)
Element が無いと言われるので追加してあげる。
// public/javascripts/application.js var Element = function(){}; // ★追加 Element.update = function (element, html) { $('#' + element).html(html); }
その他参考
will_paginate を Ajax 対応したらレスポンスを JS で返さなくちゃいけないが、基本的な知識が不足していた。 やり方としては、respond_to do |format| で format.js を指定しつつ RJS ファイルを用意してあげる。will_paginate経由でなくて、画面読み込み時に自前で Ajax 読み込みを行う場合などの html で返す場合も併記したりできて便利。以下を参考に。
そういえば、will_pagenate 経由で Ajax を行った場合、クリック時のイベントに処理を追加できないかな。読み込み中 gif とかを表示したいんだけどやり方がわからない。
Modelのバージョン管理を行う(acts_as_versioned)
やりたいのはあるモデルを更新した際に更新前の状態を自動的にバックアップしてくれる機能。
いろいろあると思うけど、acts_as_versioned プラグインを使ってみた。
環境:Rails3.0系
インストール
インストールはいつもの感じで以下
$ gem install acts_as_versioned
Gemfile に gem 'acts_as_versioned' を追加
$ bundle install
バージョン管理したい Model に以下を追加。
class Account < ActiveRecord::Base acts_as_versioned # 追加 end
バージョン管理用のマイグレーションを作成する
$ rails g migration add_version
作成したマイグレーションに以下を記述。
# db/migrate/20110912133220_add_version.rb class AddVersion < ActiveRecord::Migration def self.up Account.create_versioned_table end def self.down Account.drop_versioned_table end end
$ rake db:migrate
使いっぷり
- バックアップ数を取得は version メンバにアクセス。
- バックアップの配列取得は versions メンバにアクセス。
>> ac = Account.new >> ac.name = "baggio" >> ac.save >> ac = Account.first >> ac.version => 1 >> ac.versions => [#<Account::Version id: 1, account_id: 1, version: 1, name: "baggio", created_at: "2011-09-12 13:42:09", updated_at: "2011-09-12 13:42:09">] >> ac.name = "zola" >> ac.save >> ac = Account.first >> ac.version => 2 >> ac.versions => [#<Account::Version id: 1, account_id: 1, version: 1, name: "baggio", created_at: "2011-09-12 13:42:09", updated_at: "2011-09-12 13:42:09">, #<Account::Version id: 2, account_id: 1, version: 2, name: "zola", created_at: "2011-09-12 13:42:09", updated_at: "2011-09-12 13:42:43">]
- 全履歴を取得する例(更新時間でソート)
@histories = Account.all.map{|ac| ac.versions}.flatten.sort{|a,b| b.updated_at <=> a.updated_at}
いや、もっと簡単に Account::Version.all で良いみたい。
@histories = Account::Version.order('updated_at DESC')
履歴が追加か変更かについては、 h.account.version == 1 で判定可能。
true : 新規 false : 変更(2つ以上の履歴があるので)
でも上記だけだと削除した履歴は取得できない。
自前で削除のフックを使って記録するしかないのか。
参考
- コールバックとか応用例
Rails tips vol2
最近Railsを触っていていろいろ知ったことをメモ。
コントローラから部分テンプレートのみを render したい場合
状況としては、Ajaxのレスポンスとしてある程度複雑なhtmlをテンプレートから生成し、かつ layout の application.html.erb を含めたくない場合。render :partial を使えば良い。
render :partial => 'tasklist', :locals => {:tasks => @tasks}
部分テンプレートへの引数は :locals => で渡す事もできる。
明示的に渡さなくても部分テンプレート内で @tasks でアクセスしてれば省略可能。
余談だけど、部分テンプレートに引数を渡すか、@val でグローバルっぽくアクセスするかの判断基準でいつも迷う。その部分テンプレートがどのくらい共通で使われているかにも寄るのかな。ポリシー決めないと複数人で開発する際に迷いそう。
日本語のパラメータをモデルで検索用に変換する
パラメータで日本語文字を受け取って、モデルで検索をかけたい場合、そのまま where に入れてもエンコードされているためヒットしない。そこで、モデルで検索パラメータとして使う前に URI.decode() する
where("name = ? and status = ? and msg LIKE ?", name , StatusTable[status] , "%#{URI.decode(filter)}%")
- filter がパラメータで受け取った日本語検索対象文字列
- LIKE で検索対象文字列を含むモデルインスタンスを抽出している
Jsonでレスポンスを返す
Ajaxのレスポンスで js.erb でコントローラで生成したデータ群を特定の js メソッドに渡すだけのコードを書くくらいなら、 json で返してクライアント側で js 呼び出しに渡すのがスマートらしい。
json を使うには以下の2点に注意する。
Ajaxリクエスト時に dataType: "jsonp" を付加する
これをやらないとサーバから json を受け取ってもコールバックを呼んでくれない。
$.ajax({ type: "PUT", cache: false, url: "/tasks/" + id, data: request_str, dataType: "jsonp" //★ここ重要 });
render :json でレスポンスを返す
コントローラで json として返したいデータをハッシュで生成して render :json に渡す。
その json データを受け取って処理するクライアントサイドの javascript のメソッドを :callback として指定してあげる。
render :json => counts, :callback => 'updateCounts'
devise で rspec のテスト
ユーザ認証機能付加してくれるプラグイン devise はとても便利なんだけど、フィルターに適用するとコントローラはそれまでの rspec が通らなくなってしまう。
あらかじめあるユーザでログインした状態でコントローラのテストを行いたい場合は以下のようにする。
- https://github.com/plataformatec/deviseを参考に以下を追加。
# spec/support/devise.rb RSpec.configure do |config| config.include Devise::TestHelpers, :type => :controller end
- テストコードでは sign_in するコードを追加する。
describe "GET 'index'" do it "レスポンスが正常であること" do sign_in User.first get 'index' response.should be_success end end
Rails tips vol1
最近 Rails をいじっていていろいろ書き留めたメモ。変なこと書いてたら突っ込んであげてください。
設定値はどこに書くべきか?
- とりあえずenvironment.rb に書いてみたらコードからもテストからも見えた。
- きちっとした人はAppConfigに書くらしい。たしかにこっちに切り替えたらすっきりした。yaml で書けるし。簡単に書くと以下。
- config/settings.yml とかを作る。それぞれの環境に応じた設定値を書く。
development: default_bg_image: "viola.jpg" default_layout: "landscape" base_bg_path: "/images/bg_img/" test: default_bg_image: "viola.jpg" default_layout: "landscape" base_bg_path: "/images/bg_img/" production: default_bg_image: "viola.jpg" default_layout: "landscape" base_bg_path: "/images/bg_img/"
-
- config/initializers/00_load_config.rb を作る。先ほど書いた yml を AppConfig に読み込むようにする。
AppConfig = YAML.load_file("#{RAILS_ROOT}/config/settings.yml")[RAILS_ENV].symbolize_keys
-
- ソース内からは AppConfig[:default_bg_image] とかって見える。便利!
- 詳しくは http://d.hatena.ne.jp/babie/20100520/1274369782
2011/09/05 追記
- babie さんのコメントをいただいた configatron も使えそう。gem install configatron で入る。詳しくは以下。
リダイレクト時に params を引き継ぐ
以下のように、action の次にハッシュで繋げればよい
redirect_to :action => "user",:user => params[:user]
select_tag で選択時にsubmit したい場合
select_tag の引数として、 :onchange => 'submit();' を追加する。
<%= select_tag "user", options_from_collection_for_select(@all_user, "name", "name"),:onchange => 'submit();' %>
Rails3でJqueryを使う設定
Gemfile に以下を追記
gem 'jquery-rails', '>= 0.2.6'
コンソールから
$ bundle install $ rails g jquery:install
prototype.js 等が削除されてjquery が追加される
Rails3で rspec を使う設定
Gemfile に
gem 'rspec-rails', '>= 2.0.0'
コンソールで
$ bundle install $ rails g rspec:install
プロキシでエラーになったり 入ってなかったら root で gem install rspec-rails やってから再度挑戦。
カラムに属性を追加
マイグレーションでカラム追加用を作る
$ ruby script/generate migration AddPhoneToUsers phone:string $ rake db:migrate
タイムゾーンを日本に設定する
config/application.rb に以下を追加する。
config.time_zone = 'Tokyo' config.active_record.default_timezone = :local
- production 環境で行う場合は、キャッシュについても気をつけること。production.rb でキャッシュが有効になっているので、development 環境と動作が異なる場合がある。その場合はキャッシュをOFFしなければならない。(これでハマった・・・)
ユーザ認証機能を追加 Deviseを使う
DeviseのUserモデルにカラムを追加する
- 単に任意のカラムを migrate で追加するだけではアクセスできない
- Userクラスの attr_accessible に追加してあげる必要がある。
Passenger を使う場合の注意
- rails s の development 環境で動いたからといって安心してはいけない。
- production 環境で動かなくなったら以下を怪しむ
- passenger を使った production 環境で動かなくなったらRails.root 環境のファイルのパーミッションを疑う。すべて root ではなく任意のユーザ権限にすること。
そんな場合の不具合の症状
- sqlite のDBファイルへの書き込みアクセスができなくなる
Rails環境でバッチファイルを動かす
Railsアプリの ActiveRecordにバッチ処理で定期的にデータを入れたい場合に使える。
- バッチ処理を任意のクラスのクラスメソッドにして model 配下に置く
- rails の runner で実行する
$ /usr/local/bin/ruby script/rails runner MakeIndex.main
本番環境で実行したい場合は -e production をつける
$ /usr/local/bin/ruby script/rails runner MakeIndex.main -e production
定期実行したい場合は cron や jenkins などに上記コマンドを登録する
scopeとクラスメソッドの挙動の違い
これまで model で定義する scopeとクラスメソッドの違いについてあまりよくわからないまま使っていたんだけど、明確な挙動の違いに遭遇したのでメモしておく
例えば以下のような scopeを作りたいとする
scope :all_counts_by_name, lambda{|name| counts = {} StatusTable.each_key{|key| counts[key] = self.by_name_and_status(name,key).count } counts }
Hashへの代入文がエラーになる
Failure/Error: @task_counts = Task.s_all_counts_by_name("volpe") ArgumentError: Unknown key(s): done, waiting, todo_h, doing, todo_m, todo_l # ./spec/models/task_spec.rb:135
- 存在しない key を使って代入しようとすると怒られる
- しかしなぜかfetchを使うと上手くいく
counts.fetch(key, self.by_name_and_status(name,key.to_sym).count)
- lambda 内では Hash への代入形式が使えないのだろうか?
戻り値の型は Hash を期待しているのに ActiveRecord::Relation が返って来てしまう
- そのため以下の様なテストコードを書いていても通らない
@task_counts = Task.s_all_counts_by_name("volpe") @task_counts[:todo_h].should >= 1
- こんなエラー
Failure/Error: subject[:todo_h].should >= 1 TypeError: Symbol as array index # ./spec/models/task_spec.rb:141
- scope の戻り値の型は ActiveRecord::Relation に決まっている?
結局 scope を使うのは断念し、クラスメソッドにした
def self.all_counts_by_name(name) counts = {} StatusTable.each_key{|key| counts[key] = self.by_name_and_status(name,key).count } counts end