最近の読書(2009.08)

http://ec2.images-amazon.com/images/I/51MtlVCi45L._SL500_AA240_.jpg

レガシーコード改善ガイド

レガシーコード改善ガイド 」は主にオブジェクト指向言語を対象に 記述されたものですが、書籍内にも記載されている通り他のパラダイムの 言語にも適用できる箇所が多々あります。 またC言語についても第19章に記載があります。 自分の環境ではどうか?と言うことふまえて自動化テスト導入までの疑問点に答える形で 「レガシーコード改善ガイド」の感想を書いてみます。(主にはC言語を対象にしています。)

なぜ自動化されたテストが良いのですか?

私が若葉マークの頃は何時間あるいは何日間もかけて手動の単体テストを行っていました。 また、私が携わった多くのプロジェクトでは、今もgdbを使った 手動の単体テストが主流を占めています(自動化されたテストなんか見たこと無い!!)。 最近見たテストハーネスは対話形式で擬似API側の戻り値を選べるようになっていました。 これを書いた人はある程度テストに対して意識があったのかもしれませんが、 自動化されたテストの前ではデバッガを使ったテストと変わりはないです。

自動化されたテストは単体テストに要する時間を削減してくれます。 一度テストを書けば100回でも200回でも同じテストを行うことができるのです。 手動テストは一度きりで時間を要します。

自動化されたテストは変更に対する恐怖を減らしてくれます。「テスト実行→ コード修正→テスト実行」してエラーが無いことを確認すれば良いのです。 既存機能を破壊していないこともある程度保障できます。

これは完全に私だけだと思いますが、自動化されたテストがすべてOKだった場合、 かなり気持ちが良いです。サッカーをされている方はわかると思いますが、 ゴールするといくら辛くても全力で走れてしまうでしょう。その感覚に近いです。 (何度後輩に説明しても?な顔をされますが。。。) 作業にテンポが生まれてスムーズに作業をこなせるようになります。

自動化テストにしない手はないでしょう? いざとなればデバッガで手動テストもできるのですから。

単体テストなんてしなくても結合テストと同時消化で良いのではないですか?

「単体テスト = セグメンテーションフォルトしないことをカバーすること」と 思っている人にたまに遭遇します。そうではありません。

結合テストで確認すべきことと単体テストで確認すべきことは異なります。 単体テストでは対象となる関数の妥当性をテストします。戻り値は正しいか? Outパラメータの内容は妥当か?

結合テストでは機能確認を行います。機能によって確認事項は様々ですが、 例えばデーモンプロセスであれば、ログ出力が正常か?メモリリークはないか?等を 確認するのが結合テストです。

どうすれば自動化されたテストがあるコードにできますか?

まず第一に行うべきことはあなたが保守しているコードをテストで保護してください。 私の経験上、デバッガによる単体試験を行っている場合はテストハーネスとの結合が できているので、自動テストに移行するのは簡単です。 デバッガで対象の関数まで取り付く代わりに、対象の関数後に確認したい値を assert()関数でチェックしてあげれば良いのです。 グローバル変数を操作するのであれば意図した値になっているかを チェックしてあげれば良いし、戻り値があるのであれば 入力に対する戻り値が妥当かどうかも確認対象にすれば良いです。

extern int target_function(int);
extern int Global_Value;

int main(void)
{
    int a, b;
    b = target_function(a);
    assert(b==0);
    assert(Global_Value==200);

    return 0;
}

テストハーネスが無い場合は少し厄介で、ミドルウェアやサードパーティのAPIを 使用している場合は擬似APIを作成してコンパイル(リンケージ)してやる必要があります。

C言語でテストコードを書く場合に注意すべきことはありますか?

「main()関数からの分離」と「static関数」に注意が必要です。

まず「main()関数からの分離」ですが、 C言語ではmain()関数が特別な意味を持っておりプロセスのエントリポイントと なっています。そのため通常はmain()関数を記述しているファイル内で使用している関数は テストハーネスと結合することができません。 できる限りmain()関数を他の関数から分離するようなファイル構成にしたほうが 無難でしょう。

本番コードを変更できる場合は、#ifdefマクロを使用してテスト用コードと 本番用コードを共存させればテストハーネスと結合することが可能です。 もしくは、assert()の部分だけifdefする方法やmain()関数を無効にするようなifdef でも良いでしょう。

ファイルや関数の構成を変更できない場合は、テストハーネスと結合する時に 無理やりmain()関数を削除するという方法しかないでしょう。たとえ小さな変更でも 製品コードを手で修正することになるのであまりお勧めはできません。

// use ifdef macro
#ifdef UT
void main(void)
{
    int a = product_func1();
    assert(a == 1);
}
#else
int main(void)
{
    product_func1();
    product_func2();

    return 0;
}
#endif

「static関数」はファイル内に使用が制限される関数です。 対象となるstaticな関数を直接テストハーネスから呼びだすことはできません。

コードを書く際には本当にstaticな関数にする必要があるのかをよく吟味する必要があります。 厳格な命名規則がある場合、関数名が衝突する可能性はほとんどありませんが、 テストのためだけにstatic関数にしないという方針は違和感があります。 通常は呼び元の関数をテストハーネスから呼び出してテスト実行すればよいです。 直接呼出したい場合はmain()関数の場合のようにテスト実行の場合だけ non-staticな関数にするのがベターかもしれません。

テストコードもバージョン管理システムで管理すべきですか?

自動化されたテストが無いプロジェクトではたいていテストコードは CVS/SVN等のバージョン管理システムで管理されていません。 (ひどいところでは対象コードも管理されていない!!) できる限り製品コードと同じようにバージョン管理システムで管理するようにしてください。

これは他人事ではなくて私もできていません。 もともとテストコードが無い状態で保守を任されて...等の状況では なかなかソースコードの管理まで手が回りません。 また保守的なプロジェクトだと管理系の作業に口出しできなかったりします。 最悪、ローカルのVCSに突っ込んでおくという手があります。

こんなにもいい手法なのになぜ私しか自動化されたテストを行っていないのでしょうか?

他のメンバーはテスト手法を知らない可能性があります。 まずはあなた自身が身に付けた方法を他の人に説明するつもりでまとめてください。 そしてそれをプロジェクトのWikiやブログにまとめてください。

後輩がいる場合はその後輩に教えてあげてください。 私は追加のテストを書いてもらいました。そうすることで自動テストのやり方を 学べると共に、簡単に自動化のおいしい部分を享受することができるからです。 間違ってもいきなりテストハーネスとの結合をやらせたりはしないように。 よくはまる部分なので嫌気をさす場合があります。注意してください。

後輩に伝授することができたら、他のメンバーにも教えてあげてください。

大規模なプロジェクトでは機能ブロック単位で担当範囲が決まっています。 まずは自分が担当しているコードに対してはかならずテストコードを書くという 方針を守ってください。 保守期間への移行と共に保守する機能ブロックが増えてくる場合があります。 その際には、追加された機能ブロックに対してもテストを用意する。 そうすれば次に保守する人が楽できるでしょう。 (機能理解の助けにもなりますし、機能変更の際にも役に立ちます。)

あとは根気よく布教活動を続けていきましょう。

どうやってテストのことを学べば良いですか?

「レガシーコード改善ガイド」で学習してください。テストに関する多くのことが 学べます。学習した後に少しずつ自分のプロジェクトに適用してください。 学習する姿勢を忘れないでください。

PythonやPerlやJava、Rubyといった言語ではXUnitと呼ばれる単体テスト用の ライブラリが用意されています。ドキュメント等も充実しているので、 これらの他の言語やテスティングライブラリを学習することは有用です。 またTDDやリファクタリング等の手法も学習すると良いでしょう。 最近では多くの書籍があります。 繰り返しになりますが、学習する姿勢を忘れないでください。

最後に

環境やプロジェクト依存な部分がありわかりにくい文章になってしまいましたが、 参考になれば幸いです。