以前、GoogleTest について書いたあとは放置してたんですが
KINECTアプリケーションのテストについては、自分が把握している中ではこの一つしかありません
GoogleTest で標準出力、標準エラー出力をテストする - 質のないDiary H
記事を読む限り、KINECTから得た情報のコンパイルのテストは出来ていますが、
KINECTを使ったテストまでは至っていないようです。#それがGoogle Mockになると思いますが、その記事はまだかなぁ。。。
http://d.hatena.ne.jp/kaorun55/20110810/1312988385
ということで GoogleMock について書いてみようと思いました。
※ 意気込んではいますが、自分でもあまりわかってないので期待せず・・
GoogleMock とは
その名の通り、Google 製の C++ Mocking Framework です。
GitHub - google/googlemock: Google Mock
なんでこれを採用したかというと 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つのテストデータを用意しています。
- 左肩から左肘 (leftUpperArm) は固定
- 左肘から左手 (leftForearm) は動かす
- 一回目は leftUpperArm と同じ方向へ
- 二回目は leftUpperArm と垂直に
このような条件になったとき、leftArmIsStraight() は TRUE or FALSE のどれを返すのか、というテストになります。
まとめ
ごちゃごちゃ書いてきてわかりづらかったかもしれませんが、
ってことでした。普段 Mock 有りのテストを書いたことがなかったので
Tython のテストコードを経てなんとなくわかった気がします。
そういうんじゃねーからっていうのがありましたらコメントください。Mock 面白い!
補足
今回は「腕は伸びてるか?」「角度はどうなってる?」という時に使う所謂単体テストだったわけですが、
振る舞いテストまで登ると Mock では厳しいかもしれません。
例:ピーカブースタイルで頭を振り続けるとデンプシーロールになる!TRUE!!
体の全座標をテストデータ書くのはわりとつらい。
そういう時は OpenNI の xn::Recorder と xn::Player を使うと楽かもしれません。
むしろ OpenNI でのテストはこれが一般的なんですかね。
- 実際に Kinect を使って、その時のデータを保存(録画)する → xn::Recorder
- 録画したデータ (.oni ファイル) を再生する → xn::Player
という感じでしょうか。実はまだ使ったこと無いので詳しくはわかりません。
これを使うことで、わざわざ Kinect の前に立ったり座ったりする手間が省ける感じかな。
いつか使ってみたい機能です。