C言語でTDD
最近C言語がわからないのはあまり良くないよなということでK&R第2版をやっていたりします。評判は悪いところもあったりしますが,練習問題が豊富なのはいいですね。
こういった練習問題は求められているものが明確なのでテストを書く練習になります。ということで次のようなコードを書いたりして
yoshimi@enaga:k_and_r% cat cunit.c #include <stdio.h> void assert_equal_str(char *expected, char *actual) { while (*expected == *actual){ if (*expected == '\0'){ return; } expected++; actual++; } printf("expected %s, but got %s\n", expected, actual); } void assert_equal_int(int expected, int actual) { if (expected != actual){ printf("expected %d, but got %d\n", expected, actual); } }
例えば練習問題5.8では次のようにして使ってます
yoshimi@enaga:k_and_r% cat ex5_8.c #include <stdio.h> void assert_equal_int(int, int); static char daytab[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }; int day_of_year(int year, int month, int day) { int i, leap; leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); if (month < 1 || month > 12 || day < 1 || day > daytab[leap][month]){ return -1; } for (i = 1; i < month; i++){ day += daytab[leap][i]; } return day; } void test_day_of_year(void) { printf("day_of_year testing:\n"); assert_equal_int(365, day_of_year(2007, 12, 31)); assert_equal_int(366, day_of_year(2008, 12, 31)); printf("\tmonth error test\n"); assert_equal_int(-1, day_of_year(2007, 0, 0)); assert_equal_int(-1, day_of_year(2008, 0, 0)); assert_equal_int(-1, day_of_year(2007, 13, 31)); printf("\tmonth_day error test\n"); assert_equal_int(-1, day_of_year(2007, 1, 0)); assert_equal_int(-1, day_of_year(2007, 1, -1)); assert_equal_int(-1, day_of_year(2007, 1, 32)); assert_equal_int(-1, day_of_year(2008, 1, 32)); assert_equal_int(-1, day_of_year(2007, 2, 29)); assert_equal_int(-1, day_of_year(2008, 2, 30)); assert_equal_int(-1, day_of_year(2007, 3, 32)); assert_equal_int(-1, day_of_year(2008, 3, 32)); assert_equal_int(-1, day_of_year(2007, 4, 31)); assert_equal_int(-1, day_of_year(2008, 4, 31)); assert_equal_int(-1, day_of_year(2007, 5, 32)); assert_equal_int(-1, day_of_year(2008, 5, 32)); assert_equal_int(-1, day_of_year(2007, 6, 32)); assert_equal_int(-1, day_of_year(2008, 6, 32)); assert_equal_int(-1, day_of_year(2007, 7, 32)); assert_equal_int(-1, day_of_year(2008, 7, 32)); assert_equal_int(-1, day_of_year(2007, 8, 32)); assert_equal_int(-1, day_of_year(2008, 8, 32)); assert_equal_int(-1, day_of_year(2007, 9, 32)); assert_equal_int(-1, day_of_year(2008, 9, 32)); assert_equal_int(-1, day_of_year(2007, 10, 32)); assert_equal_int(-1, day_of_year(2008, 10, 32)); } void month_day(int year, int yearday, int *pmonth, int *pday) { int i, leap; leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); if (yearday > (leap ? 366 : 365)){ return; } for (i = 1; yearday > daytab[leap][i]; i++){ yearday -= daytab[leap][i]; *pmonth = i + 1; *pday = yearday; } } void assert_month_day( int expected_month, int expected_day, int actual_year, int actual_yearday ) { int pmonth = -1; int pday = -1; month_day(actual_year, actual_yearday, &pmonth, &pday); assert_equal_int(expected_month, pmonth); assert_equal_int(expected_day, pday); } void test_month_day(void) { printf("month_day testing:\n"); assert_month_day(2, 29, 1988, 60); assert_month_day(12, 31, 2007, 365); assert_month_day(12, 30, 2008, 365); printf("\tyearday error test\n"); assert_month_day(-1, -1, 2007, 366); assert_month_day(-1, -1, 2008, 367); } int main(int argc, char *argv[]) { test_day_of_year(); test_month_day(); return 0; }
Makefileには次のように書いておけばvimならコマンドモードで"!make %:r && ./%:r"とするだけでコンパイルして実行してくれます。環境によってはこれだけだとダメかもしれません。
CC = gcc CFLAGS = -Wall LDLIBS = cunit.c
-Wallにしておくと例題などのコードがそのままだと警告が出まくるのがちょっとめんどくさいです。もっと標準のテストフレームワークとかあるんだろうなぁ…。やっぱり最近の開発環境周りとかも含めたところに触れてある本とかも合わせて読まないと効率悪いかも。
- 作者: B.W.カーニハン,D.M.リッチー,石田晴久
- 出版社/メーカー: 共立出版
- 発売日: 1989/06/15
- メディア: 単行本
- 購入: 28人 クリック: 721回
- この商品を含むブログ (207件) を見る
第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をMySQLのInnoDBなどにしなければならない模様。
あとActiveRecordからSQLiteでBlobを保存すると画像が壊れると聞いたのでそれの実験もした。それに関しては,つかもとさんがパッチを作ってくれた模様。GJ!
それにしても今実験するのにRSpecやっぱり使わなかった。。。やっぱり標準でサポートされてることは大きい。
*1:記憶を便りに書いているので会場でやったときとは例題も違う点に注意