わんくま同盟 名古屋勉強会 #20 に参加 & LTしてきました。
1/14にわんくま同盟 名古屋勉強会 #20 に参加 & LTしてきました。
TDDワークショップ
お題はこちら
http://www.tdd-net.jp/wankuma20-tdd-workshop-assignment.html
C言語を選んだ人は4人。2組のペアを作った。
最初にもう一方のペアの方にPCUnitの使い方を簡単に説明。スクリプトでテストの雛形を自動生成できたのでそんなに時間は食わなかったと思う。
お題その1はデータが何個でも入ること。で、その2からデータは2個までに変更とかC言語にはつらいお題だった。
時間は1時間しかなかったので都合のいいように解釈して、お題2からのスタート。
引数のキーにNULLを渡したら例外発生という仕様だがCには例外がないので戻り値0で代用しようと思ったのだが、Get()の戻り値でNULLはありうる。とりあえず引数のキーにNULLを渡さないことを事前条件ということにした。(でもこれじゃテストできないから-1を返すとかでもよかったかも)
C言語とはいえデータ構造の実装なのでオブジェクト指向的に抽象データ型(Cのクラス)にしようと最初思っていたけど、コード量が増えるし時間も少ないので泣く泣くstatic変数を使ってオブジェクトは一つだけ、という仕様にした。
最初にLruCacheMap.hを作ってAPIのプロトタイプ宣言のみ書いた。
void Put(const char *key, const char *data); const char *Get(const char *key);
それから最初のテストを書くためにまずpcunit_template.rbスクリプトでテストの雛形を生成した。
$ pcunit_template.rb LruCacheMapTest -m -M
でMakefile, main.c, LruCacheMapTest.cを生成。
LruCacheMapTest.cのtest_TODO()を修正して、次のように1回のPutとGetをするテストを作った。安易なテスト名だ。
static void test_PutGet1(void) { Put("key1", "data1"); PCU_ASSERT_STRING_EQUAL("data1", Get("key1")); }
ここでmakeをするとPutとGetの実体がなくてリンクエラーになる。
LruCacheMap.cを作ってこのテストを通すための仮実装をする。
void Put(const char *key, const char *data) { } const char *Get(const char *key) { return "data1"; }
MakefileにOBJS += LruCacheMap.oを追加してmake。テストが通った。
でもこんな実装でいいわけないので次のテストを書く。今度は2回のPutとGetをするテストを作った。
static void test_PutGet2(void) { Put("key1", "data1"); Put("key2", "data2"); PCU_ASSERT_STRING_EQUAL("data1", Get("key1")); PCU_ASSERT_STRING_EQUAL("data2", Get("key2")); }
これでmakeすると当然失敗。
Suite: LruCacheMapTest Test: test_PutGet2 LruCacheMapTest.c:37: PCU_ASSERT_STRING_EQUAL("data2", Get("key2")) expected : "data2" actual : "data1" 2 Tests, 1 Failures, 0 Skipped
ここで初めてPutとGetの実装をする。キーとデータの文字列はポインタのみコピーしているのでリテラルのみ対応ということにした。
ついでに初期状態にするためのInitも実装してテストのsetupでInitを呼ぶようにした。
#define DATA_SIZE 2 typedef struct Pair { const char *key; const char *data; } Pair; static Pair pair[DATA_SIZE]; static int next_idx = 0; void Init(void) { memset(pair, 0, sizeof pair); next_idx = 0; } void Put(const char *key, const char *data) { assert(key); pair[next_idx].key = key; pair[next_idx].data = data; next_idx++; if (next_idx == DATA_SIZE) { next_idx = 0; } } const char *Get(const char *key) { int i; assert(key); for (i = 0; i < DATA_SIZE; i++) { if (pair[i].key != NULL && strcmp(pair[i].key, key) == 0) { return pair[i].data; } } return NULL; }
これで2つ目のテストも通った。
ここで時間切れ。あと何分と言われるとテンパってしまって冷静な思考ができず全然書けなかったorz
結局できたのは2個のデータをPutしてGetできることを確認しただけ。
同じキーをPutすると置き換わるという仕様のテストも実装も書けなかった。
中途半端な状態で終わってしまったが、テストファーストで書き始め→仮実装でテストをとりあえず通す→テスト追加→本当の実装をする、という手順は守れたのでよかった。
反省点はペアプロだったのに自分で説明しながら実装を全部やってしまったこと。ペアの人にコードを書いてもらって僕が後ろで口を出すというスタイルにしたほうがよかったかもしれない。退屈させてしまってたらごめんなさい。
初LT
LTはテストに関すること、というテーマだったので自作のユニットテストフレームワークのPCUnitの紹介をした。
(組み込み)C言語用のツールなんて需要あるかな?と思ったけど、知ってもらわないことには使ってもらえないので思い切ってLT申し込みした。
人前で喋るのは苦手でよく頭が真っ白になって口から言葉が出なくなってしまうので、何回も練習して口を慣れさせたら緊張したけど割と普通に喋れたのではと思う。
欲を言えば組み込みのお仕事してる人がいたらツッコミをいただきたかったけど、TDDワークショップではC言語チームもあったし、PCUnitのことを知って使ってもらえたのでやってよかったと思う。
追記
TDDワークショップのお題をC言語でもう一度やってみた。
リポジトリはこちら
https://bitbucket.org/katono/wankumatdd_lrucachemap
今度はお題その1から順番にやった。なのでコンテナが使いたいので以前自作したCSTLを使った。それから今度は複数のオブジェクトが持てるように抽象データ型にした。
お題1は何個でもデータが入るマップそのものなのでunordered_map(ハッシュマップ)を使った。ほとんどコンテナのラッパーみたいな感じ。
お題2は2個までの制限が付き、古いものから消していく仕様になったので、要素に順序のないunordered_mapは仕様に合わなくなった。そこで内部データ構造を、要素の型がキーと値のペアの構造体であるlistに変更した。これなら追加した順序を保てる。あと、お題1のテストも仕様に合わなくなったので修正した。
そういえば仕様に明記されてないけど、お題1はハッシュをデータ構造に使ったからPutとGetの計算量はO(1)だったが、お題2からはlistに変えたから計算量がO(n)になった。
お題3はPutやGetで使われたデータは最新扱いにするという仕様。これはお題2でlistに変更したからわりと簡単。List_splice()で使われたデータの要素を一番後ろにつなぎ換えてやればいい。
PCUnit 1.5.0 リリース
PCUnit 1.5.0をリリースしました。
変更点は以下の通りです。
- アサーション失敗で常にテスト関数から抜けるように修正。
- _FATAL/_RETURNの付いたマクロを廃止。
- 追加メッセージ用アサートマクロPCU_ASSERT*_MESSAGE追加。
- PCU_format/PCU_formatW追加。
- PCU_last_assertion廃止。
- PCU_FAILを以前のPCU_FAIL0と同じ仕様に変更し、PCU_FAIL以外のPCU_FAIL*を廃止。
- PCU_MESSAGEを以前のPCU_MESSAGE0と同じ仕様に変更し、PCU_MESSAGE以外のPCU_MESSAGE*を廃止。
- pcunit_template.rbでMakefile生成対応。
- VC++でビルド後イベントでテスト実行させた場合、アサーション失敗の行にジャンプできるように修正。
アサーション失敗で即テスト関数から抜けたい場合CUnitの仕様を踏襲して_FATALの付いたマクロを作ったのだけど、毎回_FATALを付けるのがめんどくさくなったので、失敗時は常に抜ける仕様にした。(以前id:bleis-tiftさんに指摘されたとおりになった)
longjmpが使えない場合にreturnに切り替えるのは工夫すればできた。つまり_RETURNは最初からいらない子だった。
_FATAL/_RETURNをdeprecatedにせずいきなり廃止にしてしまったので、PCUnitをアップグレードして既存のテストコードのコンパイルが通らなくなったら_FATAL/_RETURNを削除してください。
PCUnit 1.4.1 リリース
PCUnit 1.4.1をリリースしました。
変更点は以下の通りです。
PCUnit 1.4.0 リリース
PCUnit 1.4.0をリリースしました。
変更点は以下の通りです。
- アサートマクロの引数が複数回評価されない仕様に変更。
- 各アサートマクロの戻り値をなしに変更。
- PCU_last_assertion()を追加。これで直前のアサートマクロが失敗したかどうか知ることができる。アサートマクロの戻り値をなくした代わりに追加。
- PCU_ASSERT_OPERATOR_INTを追加。符号あり整数の比較用。
- PCU_ASSERT_OPERATOR_DOUBLEを追加。浮動小数点数の比較用。
アサートマクロの引数が複数回評価されて使いづらいことをhiroaさんに教えていただきました。ありがとうございました!
TODO
- long long対応。
- テスト結果のXML出力機能
- ドキュメントの英訳
Test-Driven Development for Embedded C の写経
Test-Driven Development for Embedded C の3、4章を実際に写経しながら動かしてみた。ただしせっかく作ったのでTDDツールはUnityではなくPCUnitを使った。
ソースはこちら。
https://github.com/katono/TDD4EC
cygwinで動作確認。動かす時はPCUnitをmake installしておいてください。
写経してみて気付いたのは、ひとつのテストケースがとても小さいこと。ひとつのテストケースにあまり詰め込まずに、テストケース作成→製品コード実装→テスト実行→リファクタリング→テスト実行 を繰り返す。小さいサイクルってこういうことか、と手を動かしてみて初めて実感。
ひとつのテストケースが小さいと、製品コードの関数も小さくモジュール化されたテストしやすいコードになる効果もありそう。TDDで開発するのはきれいな設計にするためでもあるらしい。
意図のはっきりした小さいテストケースをたくさん作る。テストケースをたくさん作るので、やはりテスト関数の自動登録ができないとめんどくさくてやってられない。自動登録スクリプト作ってよかった!
今回は写経だったけど、実際に開発でTDDやるには慣れが必要そうだ。
使うツールは同じだけど、実装後に行う「単体テスト」とは違うなと思った。単体テストほどみっちりやると工数かかってしまうからな。
PCUnitでテスト関数を自動登録するスクリプトを作った
TDD Boot Camp 東京 1.6にて、C言語チームにユニットテスト ツールとしてPCUnitを使っていただいたそうです。
お勧めしてくれた[twitter:@mayonezudaiou]さん、ありがとうございます!
プログラマーとして自分が作ったものを使ってもらえるということは本当にうれしいことですね。
TDDツールとしての感想をいただきました。
http://d.hatena.ne.jp/do_aki/20110807/1312724116
http://d.hatena.ne.jp/teyamagu/20110731/p1
テスト関数を手動で登録しなければならないというのを忘れがちになってしまう、あるいはめんどくさいというお言葉。
CUnitもCppUnitも手動で登録しなければならないのは同じなんだけど、確かに手動登録は面倒だ。
Cutterは手動登録しなくてもいいらしい。
PCUnitは組み込み用のターゲットにも使えるようにシンプルな作りなので、やはりソースのどこかにテスト関数の関数ポインタを登録しなければならない。
どうしようかと考えたところ、ソース内のtestで始まるテスト関数の定義を検索して、その中から登録されていないテスト関数を自動で書き込んで登録するようなスクリプトを作って、テストプロジェクトのビルド時に実行するようにすればいいんじゃないか、と思った。エディタで編集中のファイルを書き換えられるのはちょっと気持ち悪いしエディタがロックかけてたらまずい気がするけど気にしないでおこう。
ファイルを読み出して文字列処理をしてまた書き戻すなんてことは、スクリプト言語でやるのが楽なのでRubyで作った。ついでにテストのソースの雛形を生成するスクリプトも作った。
この2つを使えばテストコードについてはプログラマーはテスト関数を書くだけでよくなる。お約束の部分のコードをコピペしなくてもいい。
雛形生成スクリプト(pcunit_template.rb)
pcunit_template.rbの使い方は、引数にテストスイート名を1つ以上指定すると、テストスイート名.cというファイルを生成する。-pオプションを指定すると拡張子がcppになる。-mオプションを指定するとPCU_runを呼び出すmain関数を定義したmain.c(-pならmain.cpp)というファイルを生成する。
例:
$ pcunit_template.rb HogeTest PiyoTest -p -m
を実行すると、HogeTest.cpp, PiyoTest.cpp, main.cppを生成する。
自動登録スクリプト(pcunit_register.rb)
pcunit_register.rbは、テストプロジェクトのソースファイルのあるディレクトリで引数なしで実行すると、再帰的に全てのソースコードをチェックして未登録のテスト関数の登録をする。
また、main関数での各スイートメソッドの登録も自動で行う。
オプションに-d "ソースのディレクトリ"を指定すると指定したディレクトリ以下のソースを対象とする。
具体的にどのような処理を行うか例をあげると、以下のようにtest_hogeのテスト関数の定義はあるけどPCU_Testの配列に登録されていない場合、
static void test_hoge(void) { ... } PCU_Suite *HogeTest_suite(void) { static PCU_Test tests[] = { }; static PCU_Suite suite = { "HogeTest", tests, sizeof tests / sizeof tests[0] }; return &suite; }
pcunit_register.rbを実行すると、PCU_Testの配列の初期化が追加される。
PCU_Suite *HogeTest_suite(void) { static PCU_Test tests[] = { PCU_TEST(test_hoge), ←ここに追加 }; static PCU_Suite suite = { "HogeTest", tests, sizeof tests / sizeof tests[0] }; return &suite; }
また、以下のようにスイートメソッドを登録していないmain関数は、
int main(int argc, char **argv) { const PCU_SuiteMethod suites[] = { }; PCU_run(suites, sizeof suites / sizeof suites[0]); return 0; }
このようにスイートメソッドのプロトタイプ宣言とPCU_SuiteMethodの配列の初期化が追加される。
PCU_Suite *HogeTest_suite(void); ←ここに追加 int main(int argc, char **argv) { const PCU_SuiteMethod suites[] = { HogeTest_suite, ←ここに追加 }; PCU_run(suites, sizeof suites / sizeof suites[0]); return 0; }
pcunit_register.rbをビルド時に自動実行させる方法
ビルド時にスクリプトを自動で実行させるようにできれば、ビルドするだけで自動的にテスト関数が登録されるのでプログラマーは登録忘れなんか気にせずにPCUnitに関してはテスト関数だけを書けばいい。
Makefileの場合
allターゲットにて、以下のように$(TARGET)の前にスクリプト実行用ターゲットを記述すれば、make実行でコンパイル前に必ずpcunit_register.rbを実行するようになる。
Makefileがソースファイルと違うディレクトリにある場合はpcunit_register.rbのオプションに-d "ソースのディレクトリ"を指定すること。
- all: $(TARGET) + all: pcunit_register $(TARGET) + + pcunit_register: + pcunit_register.rb
Visual C++の場合
プロジェクト→XXXのプロパティ→構成プロパティ→ビルドイベント→ビルド前イベントのコマンドラインに
(rubyのパス)\ruby.exe (pcunit_registerのパス)\pcunit_register.rb
ルネサスHEWの場合
ビルド→ビルドフェーズ→ビルド順序タブで追加ボタンを押すと新規ビルドフェーズダイアログが出る。
1/4ステップ 新規カスタムフェーズの作成で次へ
2/4ステップ 単一フェーズを選択して次へ
3/4ステップ フェーズ名は適当にpcunit_registerと指定。コマンドに(rubyのパス)\ruby.exeを指定。デフォルトオプションに(pcunit_registerのパス)\pcunit_register.rbを指定。初期ディレクトリにソースのディレクトリを指定。
4/4ステップ 何も入力せずに完了
追加したビルドフェーズpcunit_registerを一番上に持ってくる。
ぼくのかんがえたさいきょうの組み込み用ユニットテストツール
TDDの勉強を始めたら、組み込み用のユニットテストツールを作りたくなってきた。
C言語用のユニットテストツールはいろいろあるのでいくつか調べてみた。
- CUnit。たぶんC言語のxUnitでは一番有名だと思う。アサートマクロが豊富。コンソールモードをTDDというより検査用ツールとして使ったことがある。テストケース毎のsetup,teardownができないのと、ASSERT失敗時の引数の値が表示できないのが残念。(setup,teardownはtrunkでは実装されているらしい)
- Cutter。使ったことはないけど、ドキュメントみるとすごく高機能みたい。でもglib必須なので組み込みでは使いづらい。
- embUnit。名前の通り、組み込み向けxUnit。標準ライブラリすら依存しないらしい。setup,teardown,RepeatedTestもあるし、これいいなーと思ったら、ASSERTに失敗するとすぐテスト関数からreturnしてしまうようだ。
- CUnit for Mr.Ando。こちらも組み込み向け。すごくシンプルでコンパクト。規模によってはこれくらいで十分なのかもしれない。でもこれもASSERT失敗でreturnしてしまう。
組み込み向けのユニットテストツールは何が重要なのだろうか?
機能?移植性?サイズの小ささ?
個人的には、CUnitはメジャーだしコンソールモードもあるので、基本的な使い方はCUnitのベーシックモードorコンソールモード + 足りない機能を補う感じがいいと思う。
あと組み込み向けに使うので、embUnitのように標準ライブラリや外部ライブラリへの依存をなくして、できるだけ多くのコンパイラでビルドできるように移植性を高くしたい。
というわけで、PCUnit(Portable C Unit)という組み込み用ユニットテストツールを作った。
リポジトリは以下。GitとMercurialの違いだけで中身はどちらも同じです。
機能・特徴はこんな感じ。CUnitユーザーにもすんなり使えるようにCUnitの仕様を真似た。
- テスト関数の作り方はCUnitと同じでvoid test_XXX(void)を作っていく。
- アサートマクロはCUnitのCU_プレフィックスをPCU_にするだけでほとんど同じ。
- setup,teardown対応。initializeとcleanupはCUnitと同じ。
- ASSERT失敗時の引数の値表示。
- printf形式で追加メッセージを表示できる。
- テスト結果のカラー表示。
- RepeatedTest対応。
- データ駆動テストのようなことをできるようにした。
- インタラクティブなコンソールモード対応。CUnitと大体同じ。
- 標準ライブラリが使えなくても、条件コンパイルでビルドできるようにした。
- ユーザー定義のputchar/getcharを指定すればシリアル入出力とかを使ってターゲット上でも実行可能。
- ビルド方法はなるべく簡単になるように、ソース丸ごとテストプロジェクトにくっつけてビルドでOK。
- 一応スタティックライブラリをビルドするMakefileも用意した。最近はHDDも大容量なのでダイナミックリンクよりも簡単に使えるほうがいいと思った。
- とりあえず動作確認したコンパイラはgcc,VC++2010,ルネサスのHEW(H8のシミュレータ),h8300-elf-gcc(KOZOS用に買ったH8/3069マイコンボード)
- zlibライセンス。
TODO
ツールの機能に凝るより、TDDの実践をすることが大事なんだけど、いろいろと凝ってしまった。ライブラリのサイズは10〜20KBくらいになった。
組み込み向けに作ったけど、ホスト用でも使えると思います。
ご意見やツッコミをいただけるとうれしいです。