todo をサンプルに UT ってか TDD のマネ
何度も todo のプロジェクト作成しとります。今度は AWDwR を見つつ、unit test を作成してみる事に。
プロジェクト作成
大体は以下のような感じで準備を。
$ ruby rails/railties/bin/rails todo $ cp rails todo/vendor/ -r $ cp scriptaculous/lib/*.js todo/public/javascripts/. $ cp scriptaculous/src/*.js todo/public/javascripts/. $ wget http://rails2u.com/tmp/active_record_ja.rb.txt $ cp active_record_ja.rb.txt todo/lib/active_record_ja.rb
で、AR の validate メセジの日本語化、というナニでやんないとイカン事を以下に。
- active_record_ja.rb をオとしてきて lib/ にコピる
- config/environment.rb に require 'lib/active_record_ja.rb' を追加
- config/environment.rb に $KCODE = 'u' を追加 (先頭??)
データベースの用意
DB 作って database.yml 修正して model 作って migrate という流れ。
DB はサーバが稼動しているマシンにて。
$ mysql -u root <<EOF > create database todo_dev default character set utf8 ; > grant all privileges on todo_dev.* to xxx@172.16.100.4 identified by 'xxx'; > EOF $
database.yml の一部。
development: adapter: mysql database: todo_dev username: xxx password: xxx host: 192.168.0.1 test: adapter: mysql database: todo_dev username: xxx password: xxx host: 192.168.0.1
model 作成
$ script/generate model list exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/list.rb create test/unit/list_test.rb create test/fixtures/lists.yml create db/migrate create db/migrate/001_create_lists.rb $ script/generate model item exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/item.rb create test/unit/item_test.rb create test/fixtures/items.yml exists db/migrate create db/migrate/002_create_items.rb $ || db/migrate 配下を以下の通り修正 db/migrate/001_create_lists.rb >|| def self.up create_table :lists do |t| # t.column :name, :string t.column :title, :string t.column :created_on, :time t.column :updated_on, :time end end def self.down drop_table :lists end end
db/migrate/002_create_items.rb
class CreateItems < ActiveRecord::Migration def self.up create_table :items do |t| # t.column :name, :string t.column :list_id, :integer t.column :note, :string t.column :completion, :boolean, :default => false t.column :position, :int t.column :created_on, :time t.column :updated_on, :time end end def self.down drop_table :items end end
修正後、テーブルを作成
$ rake migrate
生成結果の確認は略。
model の仕様と試験項目
以前のエントリで情報投入してますが、再度まとめておきます。
- list
- 主キーは id
- 0 個以上の item を保持
- 削除時には保持している item も同時に削除
- 従属する item はその position 列順に取り出す事が可能
- title 列は必須入力
- item
- 主キーは id
- list に従属
- list に対する item リストとして動作
- note 列は必須入力
- position 列はシステム側で対応
若干微妙。エントリ投下した後で修正する可能性大。
あと、AWDwR より model な UT の観点を以下に列挙。
- CRUD の試験
- validation
- ビジネスルール
これに乗っかって試験項目を羅列してみる。controller 作成時に後ヅケで試験はするはずな項目も付けておく。
- list
- Create な確認
- List.new(:title => 'title) して save
- 主キーが dup な Create の確認
- Read な確認
- List.find.all したものを順に取り出してみる
- List.find('title')
- List.find(1)
- 存在しない ID で Read
- Update な確認
- find して代入して save
- List.update(1, :title => 'title2')
- 存在しない ID で Update
- Delete な確認
- List.find(1).destroy
- 存在しない ID で Delete (無理??)
- title ナシで Create
- validate なメセジはどうやって確認??
- 削除時の動作確認
- items の取り出し
- Create な確認
- item
- CRD は略 (上記と同様)
- Update な確認
- toggle! の動作確認
- note ナシで Create
- Create 時の position の値の確認
- Delete 時の position の値の確認
test の記述
まず fixtures から、てコトで test/fixtures 見るとスデにファイルあり。
$ cat test/fixtures/lists.yml # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html first: id: 1 another: id: 2 $
という事で、とりあえず作成を。
test/fixtures/lists.yml
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html first: id: 1 title: number-1 another: id: 2 title: number-2
test/fixtures/items.yml
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html first: id: 1 list_id: 1 note: aaa completion: true position: 1 second: id: 2 list_id: 1 note: bbb completion: false position: 2 third: id: 3 list_id: 1 note: ccc completion: false position: 3 another: id: 4 list_id: 2 note: ddd completion: false position: 1
なんかテキトー感満載だが、とりあえずこれでヤッてみる。AWDwR のナニを真似て list_test.rb を修正してみる。(実は database.yml の test: を未修正でオコラれてます)
test/unit/list_test.rb
require File.dirname(__FILE__) + '/../test_helper' class ListTest < Test::Unit::TestCase fixtures :lists def setup @list = List.find(1) end # Replace this with your real tests. def test_truth assert_kind_of List, @list end end
で、実行。
$ ruby test/unit/list_test.rb Loaded suite test/unit/list_test Started . Finished in 1.739894 seconds. 1 tests, 1 assertions, 0 failures, 0 errors $
いやはや。
DB を確認してみると、fixture でナニしたデータが格納されたままになっているが、試験を動かす度に reload されている模様 (データ変えて確認してみました)。これって DB を複数メンツで共用している場合に微妙だなぁ。基本的には結合まで (development と test のナニまで?) は localhost な DB を用意して、という形になるのかなぁ。
どんどん追加。まず Create なナニを。
def test_create assert_equal 1, @list.id assert_equal "number-1", @list.title end
で、実行。
$ ruby test/unit/list_test.rb Loaded suite test/unit/list_test Started .. Finished in 0.429548 seconds. 2 tests, 3 assertions, 0 failures, 0 errors $
で試しに以下のような試験を作ってみたのだが
def test_new tmp = List.new(:title => 'number-3') tmp.save tmp = List.find(3) assert_equal 3, tmp.id assert_equal "number-3", tmp.title end
駄目な模様。AWDwR にも new して save なナニの記述がなく、これは別途調べてみた方が良いかもしれない。上記のナニはとりあえずスルーして、AWDwR の作法をトレースしてみる事に。
と、いう事は次は Update ですな。
def test_update assert_equal "number-1", @list.title @list.title = "number-1.R" assert @list.save, @list.errors.full_messages.join("; ") @list.reload assert_equal "number-1.R", @list.title end
上記を追加後、実行。
$ ruby test/unit/list_test.rb Loaded suite test/unit/list_test Started ... Finished in 0.144762 seconds. 3 tests, 6 assertions, 0 failures, 0 errors
OK ですな。だんだんコマンド打つのが面倒になりつつある (とは言え、C-p なんですが) ので rake でなんとかならんのか、と google 先生に聞いてみると
というのを教えてくれた。
基本的には rake --tasks で確認可能との事。
若干、この行
assert @list.save, @list.errors.full_messages.join("; ")
が微妙だったので調査。
assert は一番目の引数が真ならパスって意味なのか (こら)。てコトは AR の save メソドは boolean を返すんですね。
で、assert した場合は、@list.errors.full_messages.join("; ") を出力、と。
- errors は ActiveRecord::Errors というクラスとの事。
- full_messages は_Returns all the full error messages in an array. (Rails API ドキュメントより引用)_との事にてセミコロンでつなげたナニを出力、ってコトですか。
Read な確認がアレだな、と思い以下のような試験を書いた。
def test_read lists = List.find_all idx = 1 lists.each do | x, | assert_equal x.id, idx idx += 1 end end
# 少々コードが臭いがご勘弁下さいまし。
で、実行したら WARNING が。
$ ruby test/unit/list_test.rb Loaded suite test/unit/list_test Started .WARNING: find_all is deprecated and will be removed from the next Rails release (find_all at /home/rms/todo/config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/deprecation.rb:40) ... Finished in 0.85223 seconds. 4 tests, 8 assertions, 0 failures, 0 errors $
find_all って無くなるのか。代替なナニはどれなんだろうか。ここまで指摘してくれるとは有り難い限りだなぁ。Rails API ドキュメントによると find(:all) で良いとの事。
しかし何も考えずに each で取り出しゃ id 順で取れるっしょ、と決めつけてたんですがその通りに動いたな (こら)。
あと、_存在しない ID で Read_という試験 (List.find(5) みたいな)をしてみたら
ActiveRecord::RecordNotFound: Couldn't find List with ID=5
と怒られた。こんな試験はしなくても良いとゆー事か。(本当??
どんどん作る。List.update(1, :title => 'title2') な試験も書いてみた。
def test_update2 assert List.update(1, :title => 'number-1.R') assert_equal "number-1.R", List.find(1).title end
OK でした。アプデイトしても DB 上では変更かかってない、ってコトは各自が fixture を持ってれば DB 環境の共有って可能なのかなぁ。これはヤッてみないと分からんな。
こんどは destroy。
def test_destroy @list.destroy assert_raise(ActiveRecord::RecordNotFound) {List.find(@list.id)} end
む。ちょと上で書いた_存在しない ID で試験_はこうすりゃ良いのか。ってか、find 失敗したら例外になるのかよ。
を、てコトは存在しない id でアプデイトな試験って書けるな。
assert_raise(ActiveRecord::RecordNotFound) {List.update(5, :title => 'xx')}
次です。CRUD は終了ってコトで、validate を AWDwR 流に。errors.add してないんで assert を一つ略。
def test_validate assert_equal "number-1", @list.title @list.title = "" assert !@list.save assert_equal 1, @list.errors.count end
試験書いたので早速実行したみたら
$ ruby test/unit/list_test.rb Loaded suite test/unit/list_test Started ......F Finished in 1.087787 seconds. 1) Failure: test_validate(ListTest) [test/unit/list_test.rb:52]: <false> is not true. 7 tests, 14 assertions, 1 failures, 0 errors
なにー、と言いつつ app/model/list.rb 見たら空だし。ってか、ここまでコードが空っぽでも通ってしまうあたりもナニげに凄い。
もう一点。AWDwR の Keepint Tests Flexible を試してみたんですが、@lists も @first も参照できんかった。別途調査が必要。
もう少し。このアプリにおいては_ビジネスルール_なんてのはないんで少々寂しいんですが (本当か?)、とりあえずもう少し試験を書く必要あり。要件としては
- list が削除されたら item 削除されているか
- items を取得して each でバラしたら position 順に読むか
ですので、試験を書いてみる。
def test_items tmp = List.find(1) pos = 1 tmp.items.each do | i | assert_equal i, tmp.position i += 1 end tmp = List.find(2) tmp.destroy assert_raise(ActiveRecord::RecordNotFound) {List.find(2)} assert_raise(ActiveRecord::RecordNotFound) {Item.find(4)} end
あたりまえですが、動きません。とりあえず上で書いた試験項目なナニは満足しているはずなので、モデルのコードを書いてみます。
app/models/list.rb
class List < ActiveRecord::Base has_many :items, :order => 'position', :dependent => true set_field_names :title => 'タイトル' validates_presence_of :title end
app/models/item.rb
class Item < ActiveRecord::Base belongs_to :list set_field_names :note => 'やること' acts_as_list :scope => :list validates_presence_of :note end
で、試験を動かしてみるとどうなるか。
て、エラー出てるし試験なコードバグってるし。以下のように修正。
def test_items tmp = List.find(1) pos = 1 tmp.items.each do | i | assert_equal pos, i.position pos += 1 end
とほほ。ダメダメっす。試験書きながら思っていたんですが、UT なコードの妥当性は誰が検証できるんでしょうか。てか、だからこそ、のペアプロなんでしょうね。
とりあえず list の試験はデキた。
ただ、Create がデキぬ、という部分が少々ひっカカるなあ。
item 分は冗長なので略。ただ、AWDwR 的には Controller の試験も含めて色々なワザが載っているようなので試してみて log を残しておきたいかな、と。
追記
自宅環境に持ち帰って様子を見ようとしてハマった事項を以下に。
- DB 作ってない
- config/database.yml なホストの IP が修正されてない
- chroot 環境、/proc が mount されてなかった (原因不明
追記 2
item 方面なナニも以下に。
require File.dirname(__FILE__) + '/../test_helper' class ItemTest < Test::Unit::TestCase fixtures :items def setup @item = Item.find(1) end # Replace this with your real tests. def test_truth assert_kind_of Item, @item end def test_create assert_equal 1, @item.id assert_equal "aaa", @item.note assert_equal true, @item.completion assert_equal 1, @item.position end def test_update assert_equal "aaa", @item.note @item.note = "bbb" assert @item.save, @item.errors.full_messages.join("; ") @item.reload assert_equal "bbb", @item.note end def test_read items = Item.find(:all) idx = 1 items.each do | itm | assert_equal itm.id, idx idx += 1 end end def test_update2 assert Item.update(1, :note => 'bbb') assert_equal "bbb", Item.find(1).note assert_raise(ActiveRecord::RecordNotFound) {Item.update(5, :title => 'xx')} assert_equal true, @item.completion assert (@item.toggle! :completion) assert_equal false, @item.completion end def test_destroy @item.destroy assert_raise(ActiveRecord::RecordNotFound) {Item.find(@item.id)} end def test_validate assert_equal "aaa", @item.note @item.note = "" assert !@item.save assert_equal 1, @item.errors.count end def test_destroy2 Item.find(2).destroy assert_equal 2, Item.find(3).position end end