2016年10月21日金曜日

変数の値が非数 NaNかどうか調べるために、定数 NANと比較してはならない。

次に示すのは CUnitを用いたテストケースである。
/**
 * 定数 NANと比較すると偽を返すこと。
 *
 * NaNかどうか判定するために、定数 NANと比較してはならない。
 */
public void testCompareNanAndNan() {
        /* 事前条件の準備 */
        double value = NAN;

        /* 実行 */
        boolean result = (value == NAN);

        /* 事後条件の検証 */
        Assert_false("結果は偽であること。", result);
}
これを実行すると、テストは成功する。
変数 valueが定数 NANで初期化されているが、その変数を NANと比較しても真にならない。
比較演算子で NANと比較しても NaNかどうか判断できないということで、これは正しい挙動である。
なお、このテストケースは gccを使用してコンパイルしたが、定数 NANを使用するためには math.hをインクルードする必要がある。
ISOの C99よりも前のバージョンでは、処理系によっては NANが使えない。
NaNであるかどうかの正しい判定方法を次のテストケースに示す。
/**
 * NaNであることを判定できること。
 *
 * NaNかどうかの判定には isnan()を使用すること。
 */
public void testIsNan() {
        /* 事前条件の準備 */
        double value = NAN;

        /* 実行 */
        boolean result = isnan(value);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
isnan()も math.hが必要。
==比較演算子を使用して「強引に」判定する例を次に示す。
/**
 * ポインタを使用して NANと比較することで、NaNかどうかを判定できること。
 *
 * この使い方を推奨しているわけではないが、定数を代入しただけのものは
 * ビットパターンの比較により判断ができる。
 * この例ではポインタを使用したが、unionを用いても同様のことができる。
 *
 * NaNの値は1通りではないため、計算結果や外部から読み込んだ値を正しく
 * 判断するには == は使えない。
 */
public void testCompareNanAsPointer() {
        /* 事前条件の準備 */
        double value = NAN;
        double nan = NAN;

        /* 実行 */
        boolean result = (*(long long*)&value == *(long long*)&nan);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
ドキュメントコメントにも書いてあるよう、この方法では特定の条件でした判断できない。
非数 NaNを理解するための例をもう1つ示しておこう。
/**
 * NaNは 指数部の全ビットが 1で、仮数部が 0でないこと。
 *
 * これは例を示しただけで、実際にやるのは無駄。 isnan()を使うべき。
 */
public void testNanAsPattern() {
        /* 事前条件の準備 */
        double value = NAN;
        long long exponentMask = 0x7ff0000000000000;
        long long mantissaMask = 0x000fffffffffffff;

        /* 実行 */
        long long longValue = *(long long*)&value;
        boolean result = ((longValue & exponentMask) == exponentMask)
                && ((longValue & mantissaMask) != 0);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
valueを定数 NANで初期化したものだけでなく、0.0 / 0.0した結果も正しく判断できる。
printfを使って調べると、gccの場合、NANのパターンは 0x7ff8000000000000、0.0 / 0.0の結果は 0xfff8000000000000で 1ビット異なるが、いづれも NaNと判定できた。
注:value = 0.0 / 0.0; というコードはコンパイル時点でエラーになる。 分母を変数にしてその変数を 0.0で初期化するようにすれば、コンパイルが通るようになる。

普段の判定は isnan()を使用すればよいが、isnan()が使えない場合や NaNを使ったテストケースを記述するとき、この知識は役にたつだろう。
なお、上記の例で使った CUnitテストフレームワークについてはこのブログで紹介してきたが、はじめてみる人のために簡単に説明しておく。
これらのテスト関数はつぎのようなコードを含むソースコードとして記述され、softiesの CUnitライブラリとリンクされる。
#define import_lwd_unit_Assert
#define import_lwd_unit_CUnit

#include <lwd/lwd.h>
#include <lwd/unit/Assert.h>
#include <lwd/unit/CUnit.h>
#include <math.h>

/**
 * @file
 *
 * NaNの振る舞いのテスト。
 */

/****************************************************************
        test cases
****************************************************************/

// ここに、テスト関数を記述する。
// テスト関数名が異なれば複数個記述可能。

/****************************************************************
        main
****************************************************************/

public int main(int argc, char** argv) {
        return CUnit_test(argc, argv);
}
main関数のなかで CUnit_test()を呼び出すことにより、自動的にテスト関数を認識して実行する。
なおテスト関数の名前は textXXXのように先頭4文字は testでなければならない。

2016年10月16日日曜日

単体テストフレームワーク CUnit(その2)

テスト仕様をテストレポートに表示できるようになったのは、つい最近の 2016年 6月。それまではテストケースのクラス名や、テストメソッドの名称でしか表現できなかった。
レポートにテスト仕様の表示が必要なことは最初のころから感じていたが、うまい実現方法が思いつかなかった。
仕様を記録するために、専用の関数を定義しようかと考えたことがある。
たとえば、事前条件や事後条件のテスト仕様を示すために、テストコード中につぎのような関数呼び出しを記述するのである。

 Specification_postcondition("valueは 100になっていること。");

技術的には何の問題も無いが、テストコードの作りやすさや保守性を考えると冗長すぎないかと思い、まだ実現していない。
テストコードそのものがテスト仕様になるはずなので、文字列で記述せずにテストコードから仕様の記述を生成できればそのほうがよいと考えている。しかしこちらの方は技術的に難しい。
いよいよ方法が見つからないようなら上記のような関数を提供することになる。

そこで当面の間、もう少し気楽に使えるできるよう、テストケースとテストメソッドに対してドキュメントコメントで概要だけを記述できるようにした。
例)

/**
 * 2つの値を加算できること。
 */
public void testAdd() {

この程度の記述でテストレポートの内容がわかり易くなるなら、書くようになるでしょう。


過去に記述したテストケースなど、ドキュメントコメントの記述が無いものを最近のフレームワークで処理すると、テストレポートに警告文が表示されるようになっている。

折角テストがOKになっても警告文はカッコ悪いですよね。これには既存のテストコードにもドキュメントを書いてくださいという思いが込められている。


将来的には、事前条件や事後条件、実行内容もレポートできるようにして、さらには Excelや Wordのテスト仕様書、テスト報告書が自動作成できるようにしたい。

2016年10月14日金曜日

単体テストフレームワーク CUnit


Javaでコードをテストするときには JUnitを使用しているが、C/C++でもそのようなのがあったら便利だと思った。
2006年ごろ、自社製(まだ会社設立前だったが)の Java VM(TCK通してないので、公式には“もどき”)の品質が悪く、ちゃんと細部までテストをしたいなというのが始まりだった。
ネット上で検索すると沢山の CUnitが見つかり、そのうちのいくつかを試してみた。
しかし、どれもあまりパッとしなかった。 使いたくないと感じたのは、
  1. プロジェクトが放置されて久しいもの
  2. 完成度があまりにも低いもの
  3. 手続きが多くて使いにくいもの
  4. ドキュメントが全く無いもの
  5. エラーが発生したときの処理が貧弱なもの
  6. レポートを見て「ワクワク」しないもの

3の使いにくいとは、リポジトリとかテストスイートなどのオブジェクトを作り、テストケースを1個ずつ追加しなければならないのは、無精者の自分には向かないと思った。
テストケースを追加したのに、テストスイートに追加を忘れると、エラーにもならずに黙ってテスト漏れになってしまう。

しばらく我慢して使っていたがあまりにも作業効率が悪いので、自分も CUnitを作ろうということになった。(VM作りに集中せず、横道に逸れてしまうのだが)

今なら Google Testを使うでしょう。 Google Testレポートはワクワクしないけど、Softiesの CUnitの目標と共通している点も多い。 ただ当時は日文のドキュメントが多くなかったので Google Testは不採用とした。(なんと無謀な…。 皆さんは真似してはいけません。時間の無駄です。 Google Testを使いましょう。)

CUnitの目標は
  1. JUnitライクとする。(Cではアノテーションができないので 3.8ベース)
  2. リポジトリやスイートはデフォルトで不要または自動とする。
  3. JUnit同様、テスト関数の結果が他のテスト関数に影響しない。(ファイルや DBを除く)
  4. テスト中にエラーが発生してプロセスが停止しても、次にテスト関数は実行できること。
  5. 赤と緑を使った刺激的でモチベーションを維持できるレポートを出力できること。

では細かな機能紹介は別にするとして、テストケースの記述例とテストレポートのスナップショットをご覧頂きましょう。
(バージョンは違うが、以前、パッケージ一覧でも例示してある。)

テストケースのサンプル

テストプログラムの冒頭


テストケースの仕様をドキュメントコメント @fileタグで記述する。

スタブ(必要なときのみ)


フィクスチャ


テストケース


テスト関数の仕様をドキュメントコメントで記述する。
Assertクラスのコメントもテストレポートに引用される。

main関数


main関数はスタティックリンク(デフォルト)する場合に必要。
CUnitクラスの testメソッドを呼ぶだけのため、覚えるのは簡単。
main関数の中で argc, argvの内容を testメソッドを呼ぶ前に書き換えることも可能。

テストレポートのスナップショット

テストケース一覧


テスト関数一覧


テスト関数の実行結果(1)


テストレポートに事前条件の表示が無いため、事後条件の検証結果に唐突感がある。
事前条件の表示は今後の課題。
printf等で標準出力に出力した結果は、"stdout"に表示される。(この例では空)
標準エラー出力に出力した結果は、"stderr"に表示される。
malloc, free, reallocは追跡され、"heap"に表示される。 この例ではメモリリークが起きているがこれはテストケースでおきたものではなく、softiesが使用している libbfdのバグによるもの。
なお、softiesが未公開の理由の1つはこの libbfdが GPLライセンスであるため。 これを排除して softiesを公開する予定であるが、まだ時間を要する。

テスト関数の実行結果(2)


テストによりテキストファイルのほか、イメージや動画、オーディオファイルが出力されると、自動的にテストレポート中に表示される。