🖊️

はじめての CppUTest (C言語でTDD)

2022/04/26に公開

TDD が生まれる前からながーく C 言語を使っていましたが、テストは使ってませんでした。このたび、ふとしたきっかけで使ってみることになりましたので入門記事を書いてみます。誰かの参考になれば幸いです。

TDD とは?

最初に TDD をご存じない人のために TDD を説明します。TDDとはコードのチェックをするためのテストを書いて、そのチェックをパスするためにコードを開発していくという開発手法です。例えるなら学校の定期テストの問題を自分で作って、それを自分で解くような感じですね。「テストなんてもうやりたくない!」という人もいるかもしれませんが、TDDのテストは簡単な問題ばかりでしかも100点を取れるまで何回でもチャレンジできます。100点をとったご褒美は「満足感」です。最高です。

しつこくもうひとつ例を上げるとしたら、まず失敗するテストを実行するところから始まるので、「このプログラムエラーになるよ?」という状況から、なんとかこのエラーがなくなるように開発を進めるという手法とも言えます。この場合、マイナスからのスタートなのでネガティブシンキングな人にもぴったり!じゃないでしょうか。

インストール

TDDがなにかわかった(?)ところで CppUTest の使い方です。まずインストールはパッケージがあるので apt install cpputest してしまいましょう。

テストを書く

TDD といえば最初にテストを書く Test First。ですが、これは言い過ぎで何を作るのか決めるのがまず始めです。ささっと作りたいものを書きだしましょう。

  • 渡した数字に1加算してくれる関数。その名は inc()

簡単ですね。歴戦のプログラマーがこの程度の関数にテストを書く必要なんてまったくありませんが、サンプルは簡単なほうがいいのです。ここはぐっとこらえてください。

sample.cpp
// 参照:https://cpputest.github.io/manual.html#test_macros

#include "CppUTest/CommandLineTestRunner.h"

// テストグループ名の定義、「TestFuncInc」というテストを実施するよという宣言。
// ここからグループに含まれる各 TEST() を呼び出していると考えるとわかりやすいかも。
// 最後のセミコロンは忘れずに。
TEST_GROUP(TestFuncInc){
};

// 実施するテスト内容。テストグループ名(TestFuncInc)とテスト名(CheckReturnValue)
// を書く。テスト結果の表示はこの TEST(TestFuncInc, CheckReturnValue)になるので
// できるだけわかりやすい名前を書きましょう。
TEST(TestFuncInc, CheckReturnValue){
      // LONGS_EQUAL(A,B)でA==Bでないとエラーになる
      // Bがテスト対象で、Aに予想する結果を書くといい感じになります。
      // inc(10) の結果が 11 になる(EQUAL)かをチェック
      // なぜ LONGS なのかは謎だが数字の比較にはこれを使う。
      LONGS_EQUAL(11, inc(10))
}

// 私の調査が足りないのか C++ の Runner しか今のところ見つけられていないので
// ファイルも cpp にしてますが、C 言語にも対応しています。
// main() はいつもこれでOK。
int main(int argc, char **argv){
      return CommandLineTestRunner::RunAllTests(argc, argv);
}

いざコンパイル!

$ g++ -o sample sample.cpp -lCppUTest
In file included from /usr/include/CppUTest/TestHarness.h:40,
                 from /usr/include/CppUTest/CommandLineTestRunner.h:31,
                 from sample.cpp:3:
sample.cpp: In member function ‘virtual void TEST_TestFuncInc_CheckReturnValue_Test::testBody()’:
sample.cpp:15:19: error: ‘inc’ was not declared in this scope; did you mean ‘int’?
   15 |       LONGS_EQUAL(11, inc(10))
      |                   ^~~

inc()関数が無いので当然エラーになりますね。さあ、もうここからTDDは始まっているのです。実装を開始しましょう!

いざ実装!

さっきのファイル sample.cpp の5行目から以下を追加します。

sample.cpp
int inc(int x){
    return 0;
}

コンパイル。

$ g++ -o sample sample.cpp -lCppUTest && echo ok!
ok!

さあテストをひとつクリアしました。気持ちいいですねー。では実行してみましょう。

$ ./sample 

sample.cpp:19: error: Failure in TEST(TestFuncInc, CheckReturnValue)
	expected <11 0xb>
	but was  < 0 0x0>

.
Errors (1 failures, 1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)

Failureということでテストに失敗しました。実施したテストの名前とファイル名:行番号が表示されます。このようにテスト名しか出てこないので実際のテスト(LONGS_EQUAL)が何だったかわかりません。そのため、できるだけテスト名をわかりやすくするのがおすすめです。

テスト結果は expected (予想)が 11 なのに実際は 0 が渡されたよということでした。明らかにinc()の戻り値がバグってますね。ささっとFIXしましょう。

sample.cpp
int inc(int x){
    return x+1;
}

そしてコンパイル、実行。

$ g++ -o sample sample.cpp -lCppUTest 
$ ./sample 
.
OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)

おめでとうございます!見事にテストをパスしました!
これで完成!!・・・なのですが、テストが単純すぎますね。inc() は return 11 してるかもしれません。もちろん我々はそんなしょうもない実装をしていないことを知ってはいるのですが、将来プログラムが大きくなるとどんなバグが仕込まれてしまうかわかったものではありません。もうひとつだけテストを追加しましょう。

sample.cpp
TEST(TestFuncInc, CheckReturnValue){
      LONGS_EQUAL(11, inc(10))
      LONGS_EQUAL(10, inc(9))
}

そして実行。

$ g++ -o sample sample.cpp -lCppUTest 
$ ./sample 
.
OK (1 tests, 1 ran, 2 checks, 0 ignored, 0 filtered out, 0 ms)

見事成功!checksが2になっていることに心を達成感で満たしましょう。

なぜ TDD?

テストをまじめに書くと開発コードと同じ量になったりテストコードのデバッグをしたりと手間がかかるかもしれません。それでもテストを書くのは安心感と満足感だと思っています。特にCは実行するまでのハードルがスクリプト言語などにくらべて高くなる(特にコンパイル)ので製品全体のビルドをせずにテストできて安心感を得られるのはありがたいです。

単純にコードが完成したら満足ということもありますが、目に見える課題(テスト)がある状態からそれをクリアして完成したほうが満足感が高いです。ライバルとか突然の別れとかおせっかいな隣人とか困難がないとラブコメも盛り上がらないのと同じです。他にも、テストをなるべく小さくすることで小さな満足感をたくさん得られるというのもあります。TODOリストにチェックをつけて満足感を得られる人には確実に向いていますよ。

副次的な効果として、グローバル変数が跳梁跋扈するような大きなコードはテストしづらいのでテスト前提でコードを書くと自然に細分化できて再利用しやすくなります(たぶん)。

Discussion