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にしておくと例題などのコードがそのままだと警告が出まくるのがちょっとめんどくさいです。もっと標準のテストフレームワークとかあるんだろうなぁ…。やっぱり最近の開発環境周りとかも含めたところに触れてある本とかも合わせて読まないと効率悪いかも。

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

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