C言語でもレガシーでも、TDD をやってやれないことはない(レガシーコード改善成分90%、TDD成分10%)

id:goyoki さんの次になるTDD Advent Calendar jp: 2011の9日目です。
まったく自重しない素敵エントリが続いているので、ここらで息抜きをしましょう。

TDD についての理論、情緒、実践についてはすでに語られてしまったので、現場で使われた話を書きたいと思います。

前提

このお話は フィクション です。
現実によく似た光景を見たり聞いたりしたとしても、それは幻想です。幻想のはずです。幻想ということにしてくださいお願いします。

はじめに

そこには C 言語のシステムがありました。
規模にして数万行の中規模なシステム。
24時間365日動き続けることが要求されるもので、僕の仕事は、このシステムの中枢部をうまいこと改修することでした。

テストコードはあるものの、設計に大きな変更が入る前のプロダクトコードが対象となっていて、ユーティリティ関数以外のテストは全滅という、まさに焼け野原とも呼べる光景でした。
アサーションの無い、ただ動かすだけのテストコードなどもごろごろしていました。

初期設計を担当した方は魔法使いと言って差し支えのないくらいのスーパーハッカーだったのですが、いかんせん気まぐれで、すでにリリースされたシステムにはあまり興味をお持ちでない様子。

僕は最初に「この環境でプロダクトコードの改修を抜かりなくやるにはどうしたらいいだろうか?」という問いを自分に投げかけました。
結構いろんなことを考えたつもりでした。
しかし、そこそこ知識はあっても、応用力が圧倒的に不足していた時期だったので、出てきた答えは割と普通なことでした。

  • 焼け野原を地ならしして草原に変える
  • 外から見たインターフェースのテストを自動化する

今回は TDDAdventjp なので、「焼け野原を地ならしして草原に変える」ことで、システムの改修を TDD で行えた、というお話を続けます。

土壌調査1

たとえ焼け野原と言えども、テストコードがあるんだから、それはどうやって実行するものなのか調べてみる価値はあります。
すでに存在しているもの、という意味で、自分はこれを土壌調査というタスクにしました。

僕の環境では、ソースツリーがモジュール(実行ファイル、ライブラリ)ごとに別々のディレクトリとする構成になっていて、テストはそれぞれのディレクトリで make test を実行すれば走るようになっていました。
これは僥倖でした。
自動化のための基盤があったからです。

とりあえずルートディレクトリに Makefile を置いて、ルートディレクトリで make test をすれば全てのテストが走るようにしました。
これで全モジュールのテストを一度に実行できるようになりました。

土壌調査2

さらにテストコードの解析を進めました。
改修対象のモジュールは、関数ポインタ配列によるイベント駆動なアーキテクチャで、業務ロジックはイベントハンドラとして実装することになっており、テストコードはイベントハンドラと一対一に対応していました。
ユーティリティ関数は基本的に static 宣言されており、外部には見せない方針のようでした。

なお、テストコードの所感をまとめてみるとこんな感じです。

  • それぞれのテストコードに、同じ名前の変数や関数が定義されている
    • グローバル変数 (ぐぬぬ)
    • 共通ライブラリ関数 (スタブ関数として振る舞いを制御できるのでこれはよかったのかな)
  • テストランナーとなるモジュールが無い
    • main 関数を定義したテスト用ドライバを include している(ぐぬぬ)
    • テストコードそれぞれがテストランナーとテスト関数を兼ねている(ぐぬぬ)

こんな感じですね。

テストコードの内訳

問題は見て分かるとおり次の 2 点。

  • グローバル変数領域とスタブ関数領域の内容が、ほとんどのテストコードで重複してる
  • テスト関数の領域が必然的に下のほうに集まっているので読むのが面倒

構成を維持したままテスト関数を充実させるべきか、0 から書き直すべきか、結構悩みました。

テストコードの組織化

ユーティリティ関数のテストコード

比較的独立していたユーティリティ関数のテストコードについては、共通する部分をまとめて、基底クラスのように扱うことにしました。

基底クラスっぽい

ユーティリティ関数には、入力値に対する20以上のチェックを含む関数があったりしたので、テストコードに集中できるようになってよかったと思います。

イベントハンドラのテストコード

内部状態(もちろんグローバル変数)によって参照するイベントマトリクスが替わるようになっていたため、イベントハンドラのテストとして、振る舞いの検証と、内部状態(事前・事後)の検証が必要になります。
しかし、残念ながら既存のテストコードでは、イベントハンドラの返り値だけしか検証していませんでした。これではテストになっていません。

振る舞いの検証

振る舞いを検証するために、スタブ関数を活用しました。
関数呼び出しの引数を記録する、返り値を設定できるようにする、といったことです。
後で「○○の関数が呼ばれていること」みたいなテスト項目を抽出するのにも利用できました。

内部状態の検証

2 重、3 重のマトリクスからなる内部状態のため、手で書くのは最初から諦めてました。
代わりに、xls に書かれていたイベントマトリクスを Ruby でごにょごにょして、有り得る組み合わせのテスト関数を全部生成してみたら、2000 を越えました。
実行にかかる時間はそうでもないので、まあしょうがない。

そして TDD をちょっとだけ

やっとこさ本題の TDD です。
仕様はもう決められてしまっていたし、コーディング規約によってリファクタリングもできないプロジェクトだったので、所詮は似非 TDD ですが。

まず、テスト結果の出力に色を付けることにしました。失敗は赤、成功は緑。
制御コードを駆使して背景色を変えるというやつです。 ^[[32m] とか ^[[0m] とかです。よくありますよね。でもこんなものすらなかったんですよ。

次にテストランナーのコードに SEGV を握り潰すシグナルハンドラを定義しました。
テストを実行したら、とりあえず最後まで走ってほしかったからです。例外キャッチとか高度な機能は無いので高級言語羨しいなーとか思ってました。

とりあえずこれだけの準備で TDD を始めてみました。

やっぱり事前の設計通りにテストから書いてみると、どうにもうまくいかないケースがありました。
「お、これが TDD の効果か」などと戯言を言いながら、元の内部状態にぶら下げる形でさらに内部状態を追加するよう設計を変更して(もちろんテストコードも生成しなおして)、なんとかテストは通るようになりました。

ちなみに、お客様に対しては「詳細設計書は Doxygen で生成したものとする」という合意を得ていたので出来たことでした。
もし、詳細設計書として、フローチャートや状態遷移図を要求されていたら時間的にアウトでしたね。

テストをちゃんと書いたことの効果は割とあったようで、その後の結合試験において、状態異常となるような問題は発生しませんでした。これは嬉しいことでした。(その代わりにインターフェースの仕様バグがぼろぼろ出ました orz)

システムの引き継ぎ

僕が書いていたシステムの改修を別の人に任せることになったときも、引き継ぎと称して次のような時間を取りました。

  • テストコードを通じたプログラムの内部動作の説明
  • テストを書く、実装をする、問題があればそれを確かめるテストを書く、というやり方の説明

「テストが失敗すると赤くなる、成功すると緑になる」というのは、プログラミングに不慣れな人にとっても、視覚的な印象と共にモチベーションを維持するよいツールだったようです。

引き継いだ人が別の場所で書いていた文章に、「テストが全部成功して緑になった時は達成感がありました」などとあって、自分の苦労は無駄じゃなかったのかな、と少し思い直しました。

最後に

上述のとおり、ある程度の自動化されたユニットテストを整備したことで、焼け野原は今では少し干からびた芝生程度にまで回復しました。
しかし、内部実装に依存しすぎたテストコードが多々あるため、リファクタリングを行う前にテストコード自体の見直しが必要です。
僕は諸事情によりプロジェクトを離れることになってしまいもう関われないので、その点だけが心残りです。

xUTP読書会に参加して、様々な方々に教えていただいたからこそ、こういったことを考えられるようになったのだと思っています。会社の中だけでは絶対に今の自分は存在していませんでした。本当に感謝しています。

おまけ

私が自分の TDD を語ることができるようになるまで学ぶことは多く、それを待っている間に人生が終わってしまいます。
そこで、自分の学びや知識の源泉となった xUTP のエッセンスを広く伝えるため、xUTP読書会の有志と共に Web マガジンぺけまを始めました。
TDD を始めてみた人や、レガシーコードに立ち向かう人にとって、少しでも助けになれば幸いです。
なお、ぺけま自体を続けていくのも大変なので、協力者はいつでも募集中です。

本当に最後に

明日は TDDBC 北陸の主催者で、今は東京住まいな id:katzchang さんのVOYAGE GROUP エンジニアブログ : 滅びの言葉をテストするです。
素晴しく頭の切れる方なので、切れ味鋭いエントリを期待しています!