torutkのブログ

ソフトウェア・エンジニアのブログ

ソフトウェア開発雑記

UMLの開始/終了の記号が覚えられない

いままで何度となく分からなくなってその都度調べていているのが、UMLの状態遷移図やアクティビティ図における開始記号(黒丸:●)と終了記号(二重黒丸:◎の中が塗りつぶし)です。

コード実行のカバレッジ(網羅)の分類

テストを実行して、テスト対象のコードのどの部分が実行されたかを計測する際、命令網羅、分岐網羅、条件網羅、パス網羅といった言葉や、C0、C1、C2、といった略語を目にします。
先日JavaのモックツールJMockitに搭載されるカバレッジ計測機能を調べた時に、JMockitは、"Line coverage"と"Path coverage"を計測するとありました。LineとPathがどの網羅に該当するかを調べてみました。

  • 命令網羅(C0)
    • 全命令(命令語)を1回以上実行
  • 分岐網羅(C1)
    • 「条件網羅」ということもある。条件分岐で生じる経路がすべて実行される(条件文の真と偽の分岐が実行される)
  • 複合条件網羅(C2)
    • 「パス網羅」ということもある。条件分岐のすべての組み合わせが実行される

これだけだと、知っている人にしか分からない状態なので、例を挙げて考えてみます。

/**
 * 商品の課金額を算出する。
 * 価格が1500円未満の商品は送料を500円加える。
 * 会員資格がGOLDの場合、商品価格を10%割引とする。
 */
public int charge(int price, int rank) {
    int shipping = 0;
    if (price < 1500) {
        shipping = 500;
    } 
    if (rank == Member.GOLD) {
        price *= 0.9;
    }
    return price + shipping;
}

このコードにおいて、各網羅を100%とするテストケースの数は、

  • 命令網羅(C0)が100%となる場合のテストケースは1通り
    • 入力:price = 500, rank = Member.GOLD
  • 分岐網羅(C1)が100%となる場合のテストケースは2通り
    • 入力:price = 500, rank = Member.NORMAL
    • 入力:price = 2000, rank = Member.GOLD
  • 複合条件網羅(C2)が100%となる場合のテストケースは4通り
    • 入力:price = 500, rank = Member.NORMAL
    • 入力:price = 500, rank = Member.GOLD
    • 入力:price = 2000, rank = Member.NORMAL
    • 入力:price = 2000, rank = Member.GOLD

となります。

なお、この複合条件網羅(C2)に必要なテストケース数が、McCabeの循環複雑度と一致するらしく(未確認)、メソッドをシンプルに作ることがテスト工数を下げることにつながるというお話です。

ここで、当初のJMockitカバレッジですが、Lineは想定通り命令網羅(C1)、Pathは複合条件網羅(C2)でした。

単体試験(単体テストユニットテスト)のやり方いろいろ

ここでは、単体試験対象はメソッド・関数レベルとします(時々、単体試験というと、1本のプログラムとか1機能を対象にするという人がいるので、定義を明示しておきます)。単体試験をどのような環境で実施するのか、いくつか流儀があるようです。

デバッガ上で対象メソッドを実行する

聞いた話では、デバッガ上で対象コードを実行し、デバッガ上で変数の値を変更し、ステップ実行(かブレークポイントを置いて実行するか)させて、デバッガ上で挙動を見て試験結果を記載するというもののようです。デバッガ上で値の確認をすることから結果判定は人間系となってしまうようです。

コンパイルを通して実行可能とするためには、テストスタブが必要なはずですが、そこまで深く話を聞いたことがないのでどうやっているか不明です。

プリント文を埋め込んで対象メソッドを実行する

プリント文を埋め込み要所要所で変数値を出力させ、その結果を人間系で判定し試験結果を記載するというものです。デバッガが使えない環境で使うようです。単体試験が終わったらプリント文を取り除きますが、人によってはコメントアウトのみだったりするので、ソースコードが汚れます。

テスト用に手を入れて、テストが終わってから手を入れる、というやり方には賛同できませんが、そうしているケースは目にしたことがあります。

  • プリント書式を工夫して結果をスクリプト等で自動判定する応用もあります。この場合、試験実行/結果判定の自動化が可能になります。
ユニットテストフレームワークを使う

JUnitCppUnitのようなユニットテストフレームワークを使い、テスト対象メソッドを実行し結果を自動判定させます。前2者に対して試験実行と結果判定の自動化という要素が加わりますが、ローカル変数のメソッド途中での値が取得できないので、入出力が不明確なメソッド(void hoge(void))を好む開発者は使いこなせないことがあります。

単体試験でのスタブとドライバの作成

  • スタブは作らずコメントアウト
  • スタブは作らず本物を使用(それって結合試験だよね)
  • スタブをまじめに作ります
  • スタブはモックツールで簡単に済ませます

などいろいろあるようです。

スタブを作るとなると、数が莫大になるので、コストが通常の3倍、などと言われることがあります。

スタブを作らない場合で、本物が揃ってからテストするという場合、それって結合試験だよね、という話と、コードを書いたその場で単体テストをしていないので、コード作成時点とテスト実施時点が離れてしまい、開発効率の低下が気になります。

なので、理想はモックツールでスタブを作らずに済ませる方向だと考えています。

テスト代替えの種類

Microsoftのサイトに、テスト代替の種類が載っていたので、URLをメモ。
http://msdn.microsoft.com/ja-jp/magazine/cc163358.aspx

テストファーストは単体試験ではない

ただし、テストファーストで作成することによって、単体試験が容易になります。

TestNGJUnit

調べれば調べるほどTestNGに傾いています。特にカバレッジの網羅を狙うと、テストメソッドは1つで、複数の入力・期待値のデータの組を用意して、これを流せる機能がほしくなります。JUnit 4でもParameterizedクラスと@Parametersを使って実現できなくはないですが、Parameterizedを使うと、そのテストクラスに定義したメソッドすべてがデータの組の数と等しい回数呼び出されてしまい、いま三ついけてません。

などと課題はありますが、デファクトスタンダードの強み、情報の豊富さを凌駕できるかというと、そこまでではないかなぁと思ってしまいます。ツールの性能の違いが、生産性の決定的差でないということです。

んっ、Theoriesクラスがもしかすると使える?

追加メモ

http://builder.japan.zdnet.com/news/story/0,3800079086,20410767,00.htm

ダミー、スタブ、スパイ、フェイク、モックについて記載があります。
単体テストで、テスト対象が依存するコンポーネントの代替えについて振る舞いで分類していました。