Cっぽいコードでgtestとgmockを使ってみる その2

Cっぽいコードでgtestとgmockを使ってみる その2

前回Cっぽいコードのフリー関数に対してgtest, gmockを使った試験ができそうだ、というところまで進めることができましたが、やはり既存のソースコードに手が入ってしまうことが気になっていました。
今回は、そこを改良できないか試してみます。

参考にした情報

以下の情報を参考にさせていただきました。ありがとうございます。
(自分で思いつくのは無理です・・・)

方針を検討する

前回のコード

前回の試験対象関数が書かれたコードはこんな感じでした。

関数名 説明
publicMethod 試験対象の関数。内部で面倒な処理を行うprivateMethod01を呼び出している。
privateMethod01 面倒な処理をしている関数。publicMethodと結合して試験はしたくない。
#include "capsule.h"

#ifdef GTEST
// add "_impl" suffix to private function
#define FUNC(func_name) func_name ## _impl
#else
// not to change
#define FUNC(func_name) func_name
#endif // TEST

// Prototype for private function
static void FUNC(privateMethod01)(const uint8_t input, uint8_t &output);

#ifdef GTEST
// To use mock, replace function call to pointer
void (*privateMethod01)(const uint8_t input, uint8_t &output) = FUNC(privateMethod01);
#endif

// Implement for public function
void publicMethod(const uint8_t input, uint8_t &output) {
    std::cout << "publicMethod called." << std::endl;

    // call private function
    privateMethod01(input, output);

    return;
}

// Implement for private function
static void FUNC(privateMethod01)(const uint8_t input, uint8_t &output) {
    std::cout << "privateMethod called." << std::endl;

    // TODO Implement here!!
    return;
}

試験のために手が入っている部分は、

  • テストビルド時に関数名を 関数名_impl に置き換えるマクロの追加
  • 関数のプロトタイプ宣言と実装部分で上記マクロを使用するよう修正
  • PublicMethodからの呼び出し先をモックとすり替えるため、テストビルド時には関数ポインタを使用するよう修正

の3箇所です。できればこういったものをコード側から追い出したい。

順番の依存性を検討

現状のコードは、以下の順番で処理されることでモックへの置き換えを可能にしています。

  1. 呼び出し先のprivateMethod01のプロトタイプ宣言時に、関数名をprivateMethod01_implに変更する
  2. 元のprivateMethod01を関数ポインタで宣言し、ポインタの参照先をprivateMethod01_implにする
  3. publicMethodには手が入っていない(step1,2でprivateMethod01の呼び出しが関数ポインタ経由に変わっている)
  4. privateMethod01の実装時に、関数名をprivateMethod01_implに変更する

最終的にやりたいことは、「試験対象のPublicMethodから呼び出す関数を関数ポインタ経由にする」ことです。
したがって、ビルド時点ではコンパイラが

  1. 呼び出し先関数のプロトタイプ宣言(プロトタイプ宣言が書かれている場合)
  2. 呼び出し先を関数ポインタに差し替え
  3. 呼び出し元関数の実装

または

  1. 呼び出し先関数の実装(プロトタイプ宣言が書かれていない場合)
  2. 呼び出し先を関数ポインタに差し替え
  3. 呼び出し元関数の実装

の順番でコードを解釈できる必要があります。
ただし、元の実装によっては「呼び出し先を関数ポインタに差し替え」ることを実装を変えずに実施することが難しい場合があると思います。
今回のコードでは、呼び出し先関数のプロトタイプ宣言と実装が同じソースファイルに入ってしまっているため、ソースの変更をしないと「呼び出し先を関数ポインタに差し替え」ることができないケースにあたります。

参考で行われていた別の方法

C言語のテストでスタブ関数を使うためのアイデアでは、テスト対象の呼び出し元関数の引数として呼び出し先関数の関数ポインタを渡すことで、この問題をクリアしているようです。

試してみる

呼び出し元関数に引数を追加

テストコードから元のソースコードをincludeする前にマクロを定義しておくことで、呼び出し元関数の引数を追加します。
また、include後にはマクロを解除しておき、テスト中での呼び出しを書き換えないようにします。

capsule_gtest.cpp

#include <gtest/gtest.h>
#include <gmock/gmock.h>

// To avoid multiply declaration
namespace UNITTEST {

// privateMethod01の関数ポインタ
typedef void (*privateMethod01_type)(const uint8_t input, uint8_t &output);
// publicMethodが関数ポインタを引数として取るようにする
#define publicMethod(a, b) publicMethod(a, b, privateMethod01_type privateMethod01)

#include "capsule.cpp"

// テスト側は変えたくないので解除
#undef publicMethod

using ::testing::AtLeast;
using ::testing::SetArgReferee;
using ::testing::_;

// Mock class
class MockPrivate {
 public:
    MOCK_METHOD2(privateMethod01, void(const uint8_t input, uint8_t &output));
};

// Create instance
MockPrivate mock;

// Mock function
void mockPrivateMethod01(const uint8_t input, uint8_t &output) {
    std::cout << "mock private method called!!" << std::endl;
    return mock.privateMethod01(input, output);
}

class SampleTest : public ::testing::Test {
protected:
    // Mock target (private function)
    privateMethod01_type privateMethod01_ptr;
    virtual void SetUp() {
        // Overwrite function pointer by Mock
        privateMethod01_ptr = mockPrivateMethod01;
    }
    virtual void TearDown() {
        // Restore original function pointer
        privateMethod01_ptr = privateMethod01;
    }
};

TEST_F(SampleTest, test_no_01) {
    std::cout << "test start!" << std::endl;
    uint8_t input = 3;
    uint8_t output = 0;

    // Set behavior for Mock
    EXPECT_CALL(mock, privateMethod01(_, _))
        .Times(2)
        .WillOnce(SetArgReferee<1>(input))
        .WillOnce(SetArgReferee<1>(3));

    // evaluation
    // No.1
    publicMethod(input, output, privateMethod01_ptr); // 引数を加えて呼び出し
    EXPECT_EQ(3, output);
}

int main(int argc, char** argv) {
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}
} // namespace UNITTEST

マクロ等はテストコードに追い出されたので、試験対象関数のコードは以下のようになっています。
capsule.cpp

#include "capsule.h"

// Prototype for private function
static void privateMethod01(const uint8_t input, uint8_t &output);

// Implement for public function
void publicMethod(const uint8_t input, uint8_t &output) {
    std::cout << "publicMethod called." << std::endl;

    // call private function
    privateMethod01(input, output);

    return;
}

// Implement for private function
static void privateMethod01(const uint8_t input, uint8_t &output) {
    std::cout << "privateMethod called." << std::endl;

    // TODO Implement here!!
    return;
}

ビルドして実行すると、publicMethodからmockを呼ぶことができています。

$ g++ capsule_gtest.cpp -lgtest_main -lgtest -lpthread -lgmock -DDEBUG -DGTEST -o test.exe
$ ./test.exe
Running main() from gtest_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from SampleTest
[ RUN      ] SampleTest.test_no_01
test start!
publicMethod called.
mock private method called!!
[       OK ] SampleTest.test_no_01 (1 ms)
[----------] 1 test from SampleTest (7 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (11 ms total)
[  PASSED  ] 1 test.

capsule_gtest.cpp:56: ERROR: this mock object (used in test SampleTest.test_no_01) should be deleted but never is. Its address is @0x7fc22948c2c0.
ERROR: 1 leaked mock object found at program exit.

最後のエラーは、グローバル領域にモックオブジェクトを定義すると発生するもの(グローバル変数の初期化や廃棄順による)のようです。
(参考: googlemockでグローバル変数のモックオブジェクトを作れるか)
上記の参考では、Visual C++の場合の対処方法について書かれていましたが、今回の環境であるg++には適用できず・・・
また、公式を見るとフラグを用いれば良さそうな感じでしたが、効果はありませんでした。

まとめ

試験対象のソースコードに手を加えられない場合でも、gtest, gmockを使った試験を実施できないか試してみました。
一応可能そうではありますが、

  • 前回の手順とどちらが楽かは状況による
  • 最後のエラーが残っている

ため、「こうすれば万事OK」という結論には到達できていません。

Written with StackEdit.

コメント

このブログの人気の投稿

Cっぽいコードでgoogle testとgoogle mockを使ってみる

WSLにgoogle testを入れてみる