Hatena::ブログ(Diary)

質のないDiary H

2011-09-04

GoogleMock で Kinect アプリの自動テストに挑戦してみた

| 21:30 |

以前、GoogleTest について書いたあとは放置してたんですが

KINECTアプリケーションのテストについては、自分が把握している中ではこの一つしかありません

GoogleTest で標準出力、標準エラー出力をテストする - 質のないDiary H

記事を読む限り、KINECTから得た情報のコンパイルのテストは出来ていますが、
KINECTを使ったテストまでは至っていないようです。

#それがGoogle Mockになると思いますが、その記事はまだかなぁ。。。

http://d.hatena.ne.jp/kaorun55/20110810/1312988385

ということで GoogleMock について書いてみようと思いました。

※ 意気込んではいますが、自分でもあまりわかってないので期待せず・・

GoogleMock とは

その名の通り、Google 製の C++ Mocking Framework です。

no title

なんでこれを採用したかというと GoogleTest で標準出力、標準エラー出力をテストする - 質のないDiary H で書いたように、
Tython のテストは GoogleTest で行っています。
GoogleMock でも GoogleTest の API そのまま使えるし、名前からして親和性高いので。そんな理由です。

使ってみるその前に

どの場面で使っているかを簡単に。

Tython には xn::UserGenerator をラップした ty::UserContext があります。

// ty::UserContext のメンバ変数
//   xn::UserGenerator* userGenerator;

XnSkeletonJointPosition UserContext::getSkeletonJointPosition(int userId, XnSkeletonJoint joint)
{
    XnSkeletonJointPosition p;
    userGenerator->GetSkeletonCap().GetSkeletonJointPosition(userId, joint, p);
    return p;
}

そこから skeleton 情報を取得する ty::User がユーザAPIとして提供されています。

// ty::User のメンバ変数
//   ty::UserContext* context;
//   int userId;

Vector User::getSkeletonPosition(XnSkeletonJoint j)
{
    XnSkeletonJointPosition p = context->getSkeletonJointPosition(userId, j);

    if (isConfident(p)) {
        skeletonPosition[j - 1] = p;
    }

    return Vector(skeletonPosition[j - 1].position);
}

// 左肩の座標を取得
inline Vector User::positionLeftShoulder(void)
{
    return getSkeletonPosition(XN_SKEL_LEFT_SHOULDER);
}

// 左肘の座標を取得
inline Vector User::positionLeftElbow(void)
{
    return getSkeletonPosition(XN_SKEL_LEFT_ELBOW);
}

// 左手の座標を取得
inline Vector User::positionLeftHand(void)
{
    return getSkeletonPosition(XN_SKEL_LEFT_HAND);
}

// 左上腕(左肩から左肘にかけて)のベクトルを取得する
inline Vector User::skeletonLeftUpperArm(void)
{
    return positionLeftElbow() - positionLeftShoulder();
}

// 左前腕(左肘から左手にかけて)のベクトルを取得する
inline Vector User::skeletonLeftForearm(void)
{
    return positionLeftHand() - positionLeftElbow();
}

他にもいくつかメソッドあるんですが、まあこんな感じ。

ここで、ハードウェア (Kinect) 依存になっているのが、ty::UserContext::getSkeletonJointPosition() 内の userGenerator->GetSkeletonCap() です。
自動テストをするにあたり、この環境依存となっている部分をどうにかしなければなりません。
具体的にいうと、Kinect を接続していなければテストできない、ということです。それは嫌だ!

  • 居酒屋プログラミングができない
  • カラオケプログラミングができない

どっちでも Kinect 持っていったことあるんですけどね・・・まあそれはおいといて。
今回の話は、この依存箇所を GoogleMock でどうにかしよう!ということです。

Mock を作る対象を決める

Kinect から得られる(= 依存している)情報は、ユーザの情報(骨格とか座標?とか)です。
Tython でそれらを担当しているのは ty::User の position* 系メソッドです。
よって、これらのメソッドを Mock 化すれば幸福が実現するのではないか、ということです。

ty::UserContext を Mock 化してもよかったのですが、以下の理由でやってません。

  • 現時点で ty::UserContext にアクセスしているのは 以下だけ
    • tracking や calibration 時の callback 関数
    • 座標を得る ty::User::getSkeletonPosition()
  • callback のテストは別にいらないかなーって思ってる
    • 今後もうちょい作り込むと必要になるかもだけど
  • getSkeletonPosition は XN_SKEL_HEAD とかを受け取ってるだけなので、その上位の position* 系だけで問題ないだろう

本人もよくわかってない理由述べましたが、つまりめんどくさかったからです。
大事なのは「取得した座標をどうやって料理するか」なので、
座標さえ渡してくれる Mock なら細けぇこたあいいんだよ精神で。

Mock を作ってみる

GoogleMock (以下 gmock) の API は Google Mock — Google Mock ドキュメント日本語訳 をご覧ください。日本語ばっちり!

まず、ty::User を継承した MockUser を作成します。

class MockUser : public ty::User {
public:
    MockUser(void) : User(NULL) {}
    virtual ~MockUser(void) {}

    MOCK_METHOD0(positionLeftShoulder, ty::Vector(void));
    MOCK_METHOD0(positionLeftElbow, ty::Vector(void));
    MOCK_METHOD0(positionLeftHand, ty::Vector(void));
    MOCK_METHOD0(skeletonLeftUpperArm, ty::Vector(void));
    MOCK_METHOD0(skeletonLeftForearm, ty::Vector(void));
};

このように、position*系とskeleton*系のメソッドを Mock 化しています。
これで、MockUser::skeletonLeftUpperArm() などは任意の ty::Vector を返す メソッドとなり、Kinect から開放されました。

skeleton 系は position 系から導出されるメソッドなので Mock 化しなくても Kinect 依存はないのですが、
テストコード書くときに「3行書くより2行で済むならそれでいいじゃん」っていう感じです。
実際、Tython よく使うのは skeleton 系なので、それだけテストコードも skeleton メインになります。
Mock 化してもしなくてもいいけど、してたほうがすっきりするかな、っていう程度です。

Mock を使ってみる

左腕に関する Mock 化ができたので、ty::User で Mock 化していない
ty::User::leftArmIsStraight() のテストを書いてみましょう。

// 実際のコード
inline bool User::leftArmIsStraight(void)
{
    Vector forearm = skeletonLeftForearm();
    Vector upperArm = skeletonLeftUpperArm();

    return forearm.isStraight(upperArm);
}

// テストコード
TEST_F(UserTest, TestLeftArmIsStriaght) {
    MockUser object;

    EXPECT_CALL(object, skeletonLeftUpperArm())
        .WillRepeatedly(::testing::Return(ty::Vector(3.0f, 0.0f, 0.0f)));
    EXPECT_CALL(object, skeletonLeftForearm())
        .WillOnce(::testing::Return(ty::Vector(3.0f, 0.0f, 0.0f)))
        .WillOnce(::testing::Return(ty::Vector(0.0f, 3.0f, 0.0f)));

    ASSERT_TRUE(object.leftArmIsStraight());
    ASSERT_FALSE(object.leftArmIsStraight());
}

object.leftArmIsStraight() を実行すると、skeletonLeftUpperArm() と skeletonLeftForearm() が
それぞれ一回ずつよばれるので、その時返す値を EXPECT_CALL 内の ::testing::Return で指定します。
また、2つのテストデータを用意しています。

テストのイメージとしてはこんな感じ
https://cacoo.com/diagrams/Bb3xMwCFtoBe92mD-9C1A2.png

  • 左肩から左肘 (leftUpperArm) は固定
  • 左肘から左手 (leftForearm) は動かす
    • 一回目は leftUpperArm と同じ方向へ
    • 二回目は leftUpperArm と垂直に

このような条件になったとき、leftArmIsStraight() は TRUE or FALSE のどれを返すのか、というテストになります。

まとめ

ごちゃごちゃ書いてきてわかりづらかったかもしれませんが、

  • 本来 Kinect から受け取る値(座標)は Mock 化すれば Kinect いらないよね
  • 値さえ貰えればあとは書くだけ

ってことでした。普段 Mock 有りのテストを書いたことがなかったので
Tython のテストコードを経てなんとなくわかった気がします。
そういうんじゃねーからっていうのがありましたらコメントください。Mock 面白い!

補足

今回は「腕は伸びてるか?」「角度はどうなってる?」という時に使う所謂単体テストだったわけですが、
振る舞いテストまで登ると Mock では厳しいかもしれません。

例:ピーカブースタイルで頭を振り続けるとデンプシーロールになる!TRUE!!

体の全座標をテストデータ書くのはわりとつらい。

そういう時は OpenNI の xn::Recorder と xn::Player を使うと楽かもしれません。
むしろ OpenNI でのテストはこれが一般的なんですかね。

  1. 実際に Kinect を使って、その時のデータを保存(録画)する → xn::Recorder
  2. 録画したデータ (.oni ファイル) を再生する → xn::Player

という感じでしょうか。実はまだ使ったこと無いので詳しくはわかりません。
これを使うことで、わざわざ Kinect の前に立ったり座ったりする手間が省ける感じかな。

いつか使ってみたい機能です。

Connection: close