main関数をテストしたい基地外な人のためのメモ

やりたいこと

  • 稀有に発生するかもしれないエラーをテストしたい
  • ロジックは、main関数に直書きされてる
  • 元のソースコード/プロジェクトは弄ってはいけない!

例えば、次のようなコードをテストしたいとします。

// main.c
#include <stdio.h>
#include "hoge.h"

int main(int argc, char* argv[])
{
  int retval;

  if (hoge() != 0) {
    printf("SUCCESS");
    retval = 0;
  }
  else {
    printf("FAIL");
    retval = 1;
  }

  return retval;
}
  • hoge() の戻り値を書き換えて、main関数の成功/失敗をやりたい
  • それぞれの printf の引数が正しいか調べたい

これを、頑張って、実現します。

使うもの

テストコードの準備

では、一気に書きます。
うろ覚えで書いてるので、エラーになるかも!!

// test_main.c

// テストする関数(本物)をリネームする
//#define printf _old_printf
//#define hoge   _old_hoge

// 元のコードで include してるヘッダーを先読み込み
// #pragma once 等でインクルードガードされてることが前提
#include <stdio.h>
#include "hoge.h"

// テストする関数(本物)の宣言を消す
#undef printf
#undef hoge

// google test を読み込む
#include <gtest/gtest.h>
#include <gmock/gmock.h>
using ::testing::Return;

// テストする関数(モック)の宣言をする
#define printf  mock_printf
#define hoge    mock_hoge

// モックをガリガリ作る

// printf用モック
struct _mock_printf
{
  void operator()(const char* format, ...)
  {
    apply_internal(format);
  }

  MOCK_METHOD1(apply_internal, void(const char*));
} mock_printf;

// hoge用モック
struct _mock_hoge
{
  int operator()()
  {
    return apply_internal();
  }

  MOCK_METHOD0(apply_internal, int());
} mock_hoge;

// エントリーポイントをリネーム
#define main _old_main

// テストしたいコードを取り込み
#include "main.c"

// エントリーポイントを test_main に変更
#undef main
#define test_main main

// テスト用コード (google test)
TEST(main_function, fail_hoge)
{
  // hoge() は1回だけ呼ばれて 0 を返すよ
  EXPECT_CALL(mock_hoge, apply_internal())
    .Times(1)
    .WillRepeatedly(Return(0));

  // printf は1回だけ呼ばれて FAIL を貰うよ
  EXPECT_CALL(mock_printf, apply_internal("FAIL"))
    .Times(1);

  // 実行結果は 1 だよ
  EXPECT_EQ(1, _old_main(0, NULL));
}

// テスト用コード (google test)
TEST(main_function, success_hoge)
{
  // hoge() は1回だけ呼ばれて 1 を返すよ
  EXPECT_CALL(mock_hoge, apply_internal())
    .Times(1)
    .WillRepeatedly(Return(1));

  // printf は1回だけ呼ばれて SUCCESS を貰うよ
  EXPECT_CALL(mock_printf, apply_internal("SUCCESS"))
    .Times(1);

  // 実行結果は 0 だよ
  EXPECT_EQ(0, _old_main(0, NULL));
}

// テストのエントリーポイント
int test_main(int argc, char* argv[])
{
  ::testing::InitGoogleTest(&argc, argv);
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}

重要なのは、define/undefで元の関数を使わないようにすること。
インクルードガードが働くと、元のコードを読み込んでも使われないようにできます。

モック関数がグローバル領域にあるので、お行儀はかなり悪いです。
自分の環境では、実行終了後にエラーが発生しました。

予定外 GoogleMockでグローバル変数のモックオブジェクトを作れるか

あと、Google Mockは可変長引数には非対応なので、そこは工夫が必要そうです。

とりあえず、VS2012で動くことは確認したけど、他の環境だとどうだろう?
とりあえず、基地外の未来の自分のために、めもめも。

追記

リネーム用の define が無くても動くっぽい。
むしろ、標準関数をリネームすると、リンクがコケる場合があるので、消しました。