2013年5月24日金曜日

[gtest] ASSERT_* マクロで void 以外を return できるようにする

Google C++ Testing Framework の Google グループで「 non-void function でも ASSERT_ マクロが使えないのか」というのを見かけたので対応させてみました。

iutest では既に対応していたので、その実装を Google Test に移植したものになります。
※ ちゃんとチェックしていないのでビルドエラーなど出るかもしれません。

ソースコード
まずは、以下のクラスを追加します。
namespace testing
{

template<typename T>
struct AssertionReturn
{
  T value;
  AssertionReturn() {}
  AssertionReturn(const T& v) : value(v) {}
};
    
namespace internal {

template<typename T>struct AssertionFixed;

class AssertionMessage : public Message
{
public:
  template<typename T>
  AssertionMessage& operator << (T value)
  {
    Message::operator << (value);
    return *this;
  }
  template<typename R>
  AssertionFixed<R> operator << (const AssertionReturn<R>& r)
  {
    return AssertionFixed<R>(*this, r);
  }
};

template<typename T>
struct AssertionFixed
{
  AssertionMessage message;
  AssertionReturn<T> ret;
  AssertionFixed(const AssertionMessage& msg, const AssertionReturn<T>& r) : message(msg), ret(r) {}
};

} // namespace internal
} // namespace testing

続いて、AssertHelper を継承して AssertHelperEx を作ります。
class AssertHelperEx : public AssertHelper {
 public:
  // Constructor.
  AssertHelperEx(TestPartResult::Type type,
               const char* file,
               int line,
               const char* message)
    : AssertHelper(type, file, line, message) {}

  void operator=(const Message& message) const
  {
    AssertHelper::operator = (message);
  }
  template<typename R>
  R operator=(const AssertionFixed<R>& fixed) const
  {
    AssertHelper::operator = (fixed.message);
    return fixed.ret.value;
  }
private:
  GTEST_DISALLOW_COPY_AND_ASSIGN_(AssertHelperEx);
};

最後に、include/gtest/internal/gtest-internal.h の GTEST_MESSAGE_AT_ を以下のように書き換えます。
#define GTEST_MESSAGE_AT_(file, line, message, result_type) \
  ::testing::internal::AssertHelperEx(result_type, file, line, message) \
    = ::testing::internal::AssertionMessage()

簡単に解説
ASSERT_ マクロは最終的に
return
    ::testing::internal::AssertHelper(result_type, file, line, message)
        = ::testing::Message()
のようになっています。
AssertHelper::operator=(const Message& message) が void を返すので、void を返す関数でしか使用できません。

void 以外の型を返すために、特定のオブジェクトが代入されたら、そのテンプレート引数の型を返す operator = を定義しています。
イメージとしてはこんな感じ。
return
    ::testing::internal::AssertHelper(result_type, file, line, message)
        = ::testing::AssertionReturn<int>(1)
上記の例では、int を return するようになります。
今回の修正では、AssertionReturn が特定のオブジェクトになります。
ユーザーはこの AssertionReturn を使用して、戻り値の型と値を指定します。

ただ、これでは既存の ::testing::Message の機能が使えないですし、ユーザーが型指定できません。
そこで、AssertionMessage が ::testing::Messageの動作をしつつ、AssertionReturn が与えられたら AssertionFixed を返す役割を果たします。
AssertionFixed は、Message と AssertionReturn を所持しています。

最後に、AssertionHelper に AssertionFixed を受け取る operator = を定義すればよいのですが、
そのまま AssertionHelper に実装してしまうとライブラリのリビルドが必要になってしまいます。
そこで、AssertionHelper を継承した AssertionHelperEx クラスに実装しています。

使い方
ASSERT_ マクロの後ろに oerator << を使用して AssertionReturn を渡すだけです。
int ReturnTest(void)
{
    IUTEST_ASSERT_TRUE(false) << ::iutest::AssertionReturn<int>(-1);
    return 0;
}

最後に
冒頭で iutest には既に実装済みと言いましたが、AssertionReturn があるとテストがとても楽にかけます。
Google Test 使っててコレがなくてメンドクセとなってましたが、意外と楽に移植できました。

あとは、例外を使うテスティングフレームワークを使うという選択もあります。
iutest には ASSERT_ マクロが例外を使ってテストを中断するようにも設定できます。
例外を使う場合は、IUTEST_USE_THROW_ON_ASSERT_FAILURE を事前に定義してください。
ただし、return する場合と挙動が異なりますので、挙動を理解した上で使用してください。

0 件のコメント:

コメントを投稿