Entry

単体テストのやり方とか(その2)

2010年05月18日

えーと……なんだかダラダラ続きそうな勢いで2回目です。

前回は、単体テスト用のプログラムを用意して,オブジェクトの生成までをテストしたのでした。もう一度,今回作る Counter クラスの仕様を確認しておきます。

  • クラス Counter は内部に int 型の整数を保持し,値の取得とインクリメントとデクリメントのメソッドを持つクラスである。
  • オブジェクト生成時,内部の値は 0 に初期化される。
  • 内部の値を取得するには int get() const メソッドを使う。
  • インクリメントの関数は int inc() とし,インクリメントした後の値を返す。
  • デクリメントの関数は int dec() とし,デクリメントした後の値を返す。
  • オブジェクト間でコピーは行わない。

一応,規模的なことを書いておくと,こゆのは,単体込みでもせいぜい30分くらいで実装するようなクラスです。ここではかなり丁寧に書いているので,なんだかものすごくじっくりと実装しているように読めるかもしれないけれども,実際はわしゃわしゃと一気に(テストしながら)作る感覚だと思ってください。

前回までの続きなので,現在のソースも見ておくことにします。まず,test.cpp 。テストするためのソースです。

#include <iostream>
#include "Counter.h"

void
testCreate() {
  Counter cnt;
  std::cout << "ok" << std::endl;
}

int
main(int argc, char* argv[]) {
  testCreate();
  return 0;
}

続いて,Counter.h 。テストされるクラスの宣言です。

class Counter {
public:
  Counter() throw ();
  virtual ~Counter() throw ();
private:  // no implement
  Counter(const Counter& other) throw ();
  Counter& operator=(const Counter& rhs) throw ();
public:
  int get() const throw ();
  int inc() throw ();
  int dec() throw ();
private:
  int count_;
};

最後に,Counter.cpp 。テストされるクラスの定義です。

#include "Counter.h"

Counter::Counter() throw () {
}

Counter::~Counter() throw () {
}

前回まででなんとなくクラスの体裁ができたので,次は具体的にメンバ関数を実装することになります。ここで,実装する関数を何にするか迷うところですけれど,今回はすぐにテストできる関数から実装していくことにします。そうすると,仕様のうち初期化の部分はコンストラクタ(または初期化用の関数)で行うことになるけれど,外から結果を確認することができません(int get() const 関数が未実装のため)。とゆことは,テストプログラムを使ったテストもできない。一方,Counter::get() 関数は,結果が何であれ,出力を得ることができます。こっちは,テストプログラムからテストしやすいです。

つことで,次に作るのは,Counter::get() 関数にしましょう。これを作っておけば,メンバの変数 count_ の状態をいつでも参照できるようになることから,このテストが通っていれば,他の関数のテストもはかどるはず。

もっとも,現段階で「Counter::get() 関数が返す値が正しいかどうか」は,今のところテストしようがありません。Counter::get() 関数は count_ の値を返すところ,この値はどこからもセットされていないからです。入力に対する出力を検証しようがありません。

じゃ,どんなテストを書くのかというと,

Counter::get() 関数が定義できて何らかの整数値を返せること。

ということになります。これくらいしかできません。早速テストを書きましょう。

void
testGet() {
  Counter cnt;
  std::cout << "ok" << cnt.get() << std::endl;
}

単純に Counter::get() 関数を呼び出しているだけです。ここでは count_ の値が未定義だと分かっているので,返ってくる値は気にしません。コンパイルが通って,Counter::get() 関数を呼び出せればオッケーです。一応値は表示させておきます。念入りに調べるなら,整数型かどうかを調べる必要があるけれども,宣言を見れば分かることなのでテストするまでもないと思います。

前回のテストと同じく,テストをコンパイルするだけでは,リンクが通りません。関数の中身も書いておきましょう。

int
Counter::get() const throw () {
  return count_;
}

実行すると,こんな感じになります。

C:\home\aian\work\unitTestIntro>a
ok
ok2293508

最初の ok は,この前の生成テストの結果で,次の ok が今回追加したテストです。あてずっぽうな値が返ってきてますね(2293508)。ともあれ,Counter::get() は何かしらの整数値を返す関数として,実装されたことが分かります。

TDD の本では,ここで結果だけちゃんとした値を返すような関数(return 0; とかいった適当な即値のコード)を実装して,わざとテストを成功させるんですけれど,個人的に,そゆのは無意味だと思っています。わざとテストを成功させると,どこまで自分が「仕様通りに」実装したのか分からなくなってしまいそうだからです。ちゃんとした値を返すまで,テストは失敗させ続けるというわけです。

あまり実装が進まなかったけれども,最後に初期化の部分だけ書いておきましょう。これは,コンストラクタで初期値を書けばいいですね。まず,テストを書きます。

void
testInit() {
  Counter cnt;
  int ret = cnt.get();
  assert(ret == 0);
  std::cout << "ok" << std::endl;
}

ここでは,assert(3) を使って,返値を評価しています。ret の値が 0 になっていなければ,アサーションのメッセージと一緒に,プログラムが停止するんですよね(cassert を include すること)。初期化のコードはちと措いといて,コンパイルしたら実行してみましょう。初期化していないから(ちゃんと)失敗するはずです。

C:\home\aian\work\unitTestIntro>a
ok
ok2293508
Assertion failed: ret == 0, file test.cpp, line 21

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

ちゃんと失敗しましたね。ここでたまたま返値が0になることも考えられるんですけれど,その時は基本に戻って「このテストで何をテストしているのか」という問いに答えられるようにしておきましょう。ここでは無事に(?)テストが失敗したので,成功させるために初期化のコードを書くことにします。Counter.cpp 内のコンストラクタで初期化します。

Counter::Counter() throw () : count_(0) {
}

テストを実行してみる。

C:\home\aian\work\unitTestIntro>a
ok
ok0
ok

成功しました。これで,初期化のコードが仕様通りに実装されていると推測できます。注意しなくてはいけないのは,現段階でこの結果は厳密に言うと「推測」にとどまっているということです。なぜなら,このテストは Counter::get() を使っているところ,Counter::get() は「なんらかの整数を返す」というテストしか通っていないからです。Counter::get() が正確に実装されていることを知るには,ある入力に対して,常に対応する出力を出力したときに証明できるわけだけれども,そこまではテストしていません。ここでは出力がコンストラクタの入力に対応しているので,Counter::get() の挙動が正しいと推測できるだけです。

こう考えると,Counter::get() の挙動の正しさをブラックボックステストとして確かめるのは,(論理的に言って)なかなか難しかったりします。operator==() なんかについても言えることだけれども,こうした基本的な関数については,デバッガでホワイトボックス的に挙動を追いかけることも必要になるかもしれません。今回の例は,関数が1ステップしかないので,わざわざテストする必要もないと思いますが。

とゆことで,今回はここまで。最初の方は,コンパイルエラーやリンクエラーでコーディングの間違いを確認していたけれども,最後の方で assert(3) を使った検証方法を紹介しました。あたしの場合,xUnit を使わない場合は,assert(3) を使う場合が多いです。

一方,こうしたテストプログラムを書いていると,不便なところが色々と見えてくると思います。例えば,assert(3) にひとつでも引っかかったら,テスト全体が停止してしまうのは,ちと不便ですよね。また,今はテストの結果出力を自前で作っているけれど,こゆのはフォーマット化して定型的に扱いたいものです。こうした不便なところを実際に実感した上で,xUnit を使うと,ありがたみも増すというもんです。いきなり xUnit を使うべきじゃないと言っているのは,そゆ意味。

小さいクラスを実装するのに,ここまで丁寧に書くのもあれなんだけれども,一度しっかり単体テストを見直すにはいい機会なんじゃないかと思います。あと2回くらいで終わる予定。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN