2015年6月16日火曜日

[C++] Private な関数のテスト

本記事で扱うテスティングフレームワーク
Google Test
iutest (Google Test ライクなヘッダーオンリーテスティングフレームワーク)

C++ でテスト書いていると問題になるのが、private なメンバー関数をどうテストするか、です。
成功法としては、テストクラスを friend することです。
間違っても #define private public などとしてはいけません。

friend
Google Test の場合、テストクラスを friend する FRIEND_TEST マクロが用意されています。
また、テストフィクスチャクラスに対して friend 宣言すれば、そのテストケースではアクセス可能になります。

参照: GoogleTestでprivateメンバ関数をテストする | Geospatial屋
詳しいことは参照先を見ていただくとして、簡単に使い方を例として示します。

例1: FRIEND_TEST
// テスト対象クラス
class Hoge
{
   FRIEND_TEST(HogeTest, A); // TEST(HogeTest, A) から見えるようにする
   FRIEND_TEST(HogeTest, B); // TEST(HogeTest, B) から見えるようにする
private:
   int GetA() { return 1; }
   int GetB() { return 2; }
};

// テスト
TEST(HogeTest, A)
{
   Hoge hoge;
   ASSERT_EQ(1, hoge.GetA());
}
TEST(HogeTest, B)
{
   Hoge hoge;
   ASSERT_EQ(2, hoge.GetB());
}
テスト毎に FRIEND_TEST しないといけないので、ちょっと面倒ではありますが private メンバーにアクセスできるようになります。
例では書きませんでしたが、テストフィクスチャを使った場合(TEST_F)でも使用できます。
(※ 型付けテストでは使用できません。)

例2: friend TestFixture
2番目の方法は、テストフィクスチャクラスを friend する方法です。
// テスト対象クラス
class Hoge
{
   friend class HogeTest; // HogeTest フィクスチャーに対して friend
private:
   int GetA() { return 1; }
   int GetB() { return 2; }
};

// テスト
class HogeTest : public ::iutest::Test 
{
protected:
    Hoge hoge;
    // アクセサを書く
    int GetA() { return hoge.GetA(); }
    int GetB() { return hoge.GetB(); }
};
TEST_F(HogeTest, A)
{
    ASSERT_EQ(1, GetA());
}
TEST_F(HogeTest, B)
{
    ASSERT_EQ(2, GetB());
}
テストフィクスチャが必要になるので、TEST(Hoge, A) のようなテストには使えませんが、
FRIEND_TEST の時とは違ってテストケースすべてに対してアクセスを許可することができるので、使い勝手は良いです。

ただ、テストフィクスチャクラスでアクセサを書く必要があります。これは Google Test がテスト毎にテストフィクスチャを継承したクラスを定義しているからです。
とはいえ、一度テストフィクスチャを定義してしまえば使い回しが可能ですし、テストコード側の変更だけでテストを追加できるのが、この方法のメリットでしょう。

iutest でも、もちろん上記方法が使えます。

Google Test の問題
Google Test が用意している FRIEND_TEST マクロは、型付けテストには使えません
template<typename T> をつければいいだけなので、以下のようにマクロを追加すれば OK です。

#define FRIEND_TYPED_TEST template<typename T>FRIEND_TEST

iutest は、FRIEND_TYPED_TEST に対応済みです。

プロダクトコードに変更を加えない方法
さて、friend を使った方法は、プロダクトコード側に friend 宣言をする必要がありました。
その程度の記述は大したことないかもしれませんが、やはりプロダクトコードを1行も変えずにアクセスできたら便利ですよね。
(テスト名を書かないといけないのも個人的には面倒だなと思います。)

iutest では非侵入的に private メンバーにアクセスする方法を提供しています。


// テスト対象クラス
class Hoge
{
private:
   int GetA() { return 1; }
   int GetB() { return 2; }
};

typedef int (Hoge::* HogePrivateFunc)(); // iutest v1.12.0 からは typedef 不要になります
IUTEST_MAKE_PEEP(HogePrivateFunc, Hoge, GetA);
IUTEST_MAKE_PEEP(HogePrivateFunc, Hoge, GetB);

// テスト
IUTEST(HogeTest, A)
{
   Hoge hoge;
   IUTEST_ASSERT_EQ(1, IUTEST_PEEP_GET(hoge, Hoge, GetA)());
}
IUTEST(HogeTest, B)
{
   Hoge hoge;
   IUTEST_ASSERT_EQ(1, IUTEST_PEEP_GET(hoge, Hoge, GetB)());
}

なんか記述量増えてるし、マクロやばそうと思われる方もいると思いますが、
この方法であれば、完全にプロダクトコードを汚すことなく private なメンバーのテストが書けます。

private メンバーにアクセスする方法は、こちらで紹介されている方法を使いました。
privateメンバに外部から非侵入的にアクセスする - redboltzの日記 (archive)
 (追記: Wayback Machine にアーカイブが残っていた)

いや~凄いですね。
さて、来週に iutest v1.12.0 のリリースを予定してます。
正規表現アサーションの他、いろいろ更新されてますのでよろしくお願いします。

6 件のコメント:

  1. そもそもオブジェクトのI/Fではないものを単体試験の対象にすべきじゃありませんよ。
    xUnitでせっかく可能になったリファクタリングができなくなります。
    例えばprivate関数の条件を全て網羅できたからといって、private関数を呼び出しているpublic関数をprivate関数分だけ省略したとしましょう。private関数の実装を呼び出し元のpublic関数が吸収すればもう試験ケースは使えなくなってしまいます。

    返信削除
    返信
    1. コメントありがとうございます。
      ご指摘のとおり、private メンバーをテストするのは良い状態ではないと思います。
      (実際、自分でも書くことはほとんどないですね。)

      本記事では良し悪しは別として、テスティングフレームワークではそのようなケースにも対応しており、その機能の紹介をする意図でした。
      なので、ガンガン private メンバーのテストもしていこうぜ!と推奨しているわけでじゃないです。
      ただ、ちょっと説明不足でしたね。すみません。

      削除
  2. >そもそもオブジェクトのI/Fではないものを単体試験の対象にすべきじゃありませんよ。
    そんな訳ありません。必要があるからマクロ定義されているんです。
    妥当性は個別に判断すれば良いことです。

    返信削除
  3. http://opencv.jp/googletestdocs/advancedguide.html#adv-testing-private-code
    >ソフトの内部的実装を変更しても,ユーザからその変更が見えない限りは,テストは失敗するべきではありません.よって, ブラックボックステ>
    ストの原則 に従い,大部分のコードテストは public インタフェースを使って行われるべきです.
    >もし,まだ内部的な実装コードをテストする必要があるならば,そうしなくてもよい設計がないかどうか考えてください.しかし,どうしても pub
    lic ではないインタフェースを通してテストを行わなければならない場合は,そうすることもできます.2つの場合を考えてみましょう:

    できるからしていいという問題ではありません。
    できるからしていいのであれば、reinterpret_castでオブジェクトの中身を
    操作することだって可能なのだから良いという話になります。
    本来の目的に沿い効果的に開発するのであれば、試験すべきではありません。
    google testの開発元もやむおえない限り非推奨としています。

    返信削除
  4. 訂正します。
    ×google testの開発元もやむおえない限り非推奨としています。
    ○google testの開発元もやむおえない限ってで、基本的に非推奨としています。

    返信削除
  5. ×やむおえない
    ○やむをえない(やむを得ない、止むを得ない)

    反論、批判的な回答をする場合こそ、言葉には気を付けましょう。
    また、訂正するときにも、気を付けましょう。

    返信削除