Entry

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

2010年06月08日

忘れた頃にやってくる地味な企画。前回は,少しだけクラスの中身を書いたのでした。今回も基本的にやり方は変わらないんですけれど,せっかくだから全部作ることにしましょう。

しつこいけれども,今作っている Counter クラスの仕様です。

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

ソースが少し長くなったので,作ったところは前回のエントリをご参照をば。

今作っているクラスは,基本的にブラックボックスなテストをしながら実装しています。ブラックボックスということは,private な関数や変数は,外側から直接参照することができないことに注意する必要があります。ここら辺は,面倒くさい話がいろいろとあるので,また別の機会に検討することにしましょう。

とゆわけで,話を進めます。実装するクラスで残っているのは,インクリメント/デクリメントする関数です。例によって,仕様に従ってまずテストを書きます。

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

ここでも assert(3) でテストしています。inc() 関数を1回呼んでいるので,戻りは1になるはずです。また,get() 関数を呼んだら,これも1が返ってくるはず。テストを書いたので,中身を書いておきましょう。

int
Counter::inc() throw () {
  return count_++;
}

間違えたコードを書いています。よくある間違いですね。それでは,コンパイルしてテストを実行しましょう。

C:\home\aian\work\unitTestIntro>a
ok
ok0
ok
Assertion failed: ret == 1, file test.cpp, line 30

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

ちゃんとテストが失敗しています。アサーションの前にある"ok"は,その前に実行したテストです。前回のエントリを参照してください。

行番号が書かれているので,どのテストでコケたのか分かります。ここでは,インクリメントする順番がいけなかったのでした。インクリメントする単項演算子(++)は,変数の前に置かないといけません。直しておきましょう。

int
Counter::inc() throw () {
  return ++count_;
}

コーディング規約によっては,インクリメントの単項演算子を前に置くことを禁止しているところもあるかもしれません(よく間違えるから,後置に限定している)。反対に,C++ の場合は,速度的な考慮から前置することを勧めているところもあります。いずれにしても,どっちがどっちか自信がない向きは,分かりやすく命令を分けた方がいいと思います。ともあれ,これで動くはず。

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

今度は通りました。やれやれ(わざとらしい)。

同様にして,デクリメントの関数 dec() も書いておきましょう。まずテストから。

void
testDec() {
  Counter cnt;
  {
    int ret = cnt.dec();
    assert(ret == -1);
  }
  {
    int ret = cnt.get();
    assert(ret == -1);
  }
  std::cout << "ok" << std::endl;
}

続いて中身です。これは簡単ですね。

int
Counter::dec() throw () {
  return --count_;
}

さて,これで一連の実装が終わりました。最後に,テストに仕様漏れがないかチェックしておきましょう。なお,やる人はいないでしょうけれど,今回作ったテストは,ここで実装が終わったからといって削除してはいけません。というのも,将来的にこのソースに変更を加えた場合に,このテストでデグレ(degrade)をチェックする必要があるからです。

もうひとつ指摘しておくと,今回の仕様では,内部のカウンタがオーバーフローした場合の仕様について何も書いてありません。例外を投げるのが正しいのか,それとも無視してもいいのか,仕様からは分からないんですね。これは開発現場の裁量や,設計方針によるので,一律に言えないんですけれど,実装部門からしたら設計者に確認する必要があるかもしれません。「ごめん書き忘れた,例外投げて」と言うかもしれないし,「そんなに大きな値は扱わないし速さを優先するから考慮する必要はないよ」と言うかもしれない。自分で設計している場合は,その方針をコメントに書いておくといい。

最後に,今回作ったテストとクラスのソースを全部載せておきます。まずテスト。

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

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

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

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

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

void
testDec() {
  Counter cnt;
  {
    int ret = cnt.dec();
    assert(ret == -1);
  }
  {
    int ret = cnt.get();
    assert(ret == -1);
  }
  std::cout << "ok" << std::endl;
}

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

続いて,クラスの宣言(Counter.h)。

class Counter {
public:
  Counter() throw ();
  virtual ~Counter() throw ();
private:
  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 () : count_(0) {
}

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

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

int
Counter::inc() throw () {
  return ++count_;
}

int
Counter::dec() throw () {
  return --count_;
}

テストの方がクラスの本体よりも分量が多いですね。複雑なケースになると,ひとつの関数に対して色々なテストをする必要があるので,もっと増えることがあります。分量だけから判断するのもアレなんだけれども,単体テストを通したコードと通さないコードの品質の違いがなんとなく分かるんじゃないでしょうか。テストをしながら実装すると,それなりの水準のコードを書きやすくなるというわけです。

ここで重要なのは,「テストを書きながら実装すると,品質の高いコードが勝手にできる」ということではありません。テストをする(書く)のは,あくまでも人間であるプログラマですから,「ざるテスト」しか作れないプログラマが書いたコードは,当然品質を保証できなくなるし,テストにバグを作りこむ可能性があることも念頭に置く必要があります。その意味でも,プログラマは「自分が今何を作っているのか」や「どんな動きが正しいのか」といったことを常に頭に置いておく必要があるし,「そのためにはどのようなテストをすればいいのか」といったことについても考える必要があります。

えらそうにくどくどと書いたけれども,そんなところ。単体テストのコツとして,もう1回くらい書くつもり。

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