sioaji2012のブログ

普段は組み込み開発でC言語のみです。主にプログラムや勉強日記です

組込C言語でUnitTest 5 GoogleMock

モック

内部でsum()関数 をコールするcheck()関数 をテストする場合に、まだsum()関数が実装されていない場合などで、実際のsum()関数の代わりになるモックを作成してcheck() 関数のテストを行う。 という事をしてみます。

今回は、モックの作成方法を確認する為に、モックを作成しないで、 実際のsum()関数をコールしてcheck()関数をテストするコードを先に書きました。

check.c

#include "check.h"
#include "sum.h"

int check(int a, int b, int c)
{
  int result = 0;

  if ( sum(a,b) >= c ) 
  {
      result = 1;
  }
  else
  {
      result = 0;
  }

  return (result);
}

check.h

#ifndef CHECK_H
#define CHECK_H

extern int check(int a, int b, int c);

#endif //CHECK_H

テストコード

// テスト対象となる関数 check のためのフィクスチャ
class CheckTest : public ::testing::Test {
protected:
// 以降の関数で中身のないものは自由に削除できます.

    // コンストラクタ(初期化用)
    CheckTest()
    {// テスト毎に実行される set-up をここに書きます.
        printf("[ 初期化   ]\n");
    }

    // デストラクタ(終了処理用)
    virtual ~CheckTest()
    {// テスト毎に実行される,例外を投げない clean-up をここに書きます.
        printf("[ 終了処理 ]\n");

    }

    // コンストラクタとデストラクタでは不十分な場合.
    // 以下のメソッドを定義することができます:

    virtual void SetUp()
    {// このコードは,コンストラクタの直後(各テストの直前)に呼び出されます.
        printf("[ SetUp    ]\n");
    }

    virtual void TearDown()
    {// このコードは,各テストの直後(デストラクタの直前)に呼び出されます.
        printf("[ TearDown ]\n");

    }
// ここで宣言されるオブジェクトは,テストケース内の全てのテストで利用できます.
};

TEST_F(CheckTest, add1)
{
    printf("■CheckTest 加算テスト\n");
    EXPECT_EQ(1, check(1, 2, 3));
    EXPECT_EQ(0, check(1, 2, 4));
}

TEST_F(CheckTest, minus1)
{
    printf("■CheckTest 減算テスト\n");
    EXPECT_EQ(1, check(2, -1, 1) );
    EXPECT_EQ(0, check(2, -1, 2) );
}

テスト結果

■GoogleTestを開始しました
[==========] Running 4 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from SumTest
[ RUN      ] SumTest.add1
[ 初期化   ]
[ SetUp    ]
■SumTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.add1 (0 ms)
[ RUN      ] SumTest.minus1
[ 初期化   ]
[ SetUp    ]
■SumTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.minus1 (0 ms)
[----------] 2 tests from SumTest (0 ms total)

[----------] 2 tests from CheckTest
[ RUN      ] CheckTest.add1
[ 初期化   ]
[ SetUp    ]
■CheckTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] CheckTest.add1 (0 ms)
[ RUN      ] CheckTest.minus1
[ 初期化   ]
[ SetUp    ]
■CheckTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] CheckTest.minus1 (0 ms)
[----------] 2 tests from CheckTest (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 2 test cases ran. (0 ms total)
[  PASSED  ] 4 tests.

モック作成

テストコードのファイル CheckTest.cpp にモックを追加します。 (namespace 内に追加)

リターンを制御する為のクラスを使う

using ::testing::Return;

モッククラスを追加

/* Mocksum モッククラス */
class MockSum
{
public:
  MOCK_METHOD2(sum, int(int a, int b) );
}mock;
  • MOCK_METHOD2 の 2 は引数の数です。
  • モック対象の関数名:sum
  • モック関数の型:int(int a, int b)

モック関数を定義

/* モック関数 */
int Mock_sum(int a, int b)
{
    return mock.sum(a, b);
}

テストフィクスチャ

// テスト対象となる関数 check のためのフィクスチャ
class CheckTest : public ::testing::Test {
protected:
// 以降の関数で中身のないものは自由に削除できます.
    int (*saved_sum)(int a, int b);

    // コンストラクタ(初期化用)
    CheckTest()
    {// テスト毎に実行される set-up をここに書きます.
        printf("[ 初期化   ]\n");
    }

    // デストラクタ(終了処理用)
    virtual ~CheckTest()
    {// テスト毎に実行される,例外を投げない clean-up をここに書きます.
        printf("[ 終了処理 ]\n");

    }

    // コンストラクタとデストラクタでは不十分な場合.
    // 以下のメソッドを定義することができます:

    virtual void SetUp()
    {// このコードは,コンストラクタの直後(各テストの直前)に呼び出されます.
        printf("[ SetUp    ]\n");
        saved_sum = sum;
        sum = Mock_sum;
    }

    virtual void TearDown()
    {// このコードは,各テストの直後(デストラクタの直前)に呼び出されます.
        printf("[ TearDown ]\n");
        sum = saved_sum;
    }
// ここで宣言されるオブジェクトは,テストケース内の全てのテストで利用できます.
};
  • SetUpで、関数ポインタをモックに切り替え
  • TearDownで、元の関数ポインタに戻し

テストケース

TEST_F(CheckTest, add1)
{
    printf("■CheckTest 加算テスト\n");
    EXPECT_CALL(mock, sum(1,2)).WillRepeatedly(Return(3));
    EXPECT_EQ(1, check(1, 2, 3));
    EXPECT_EQ(0, check(1, 2, 4));
}

TEST_F(CheckTest, minus1)
{
    printf("■CheckTest 減算テスト\n");
    EXPECT_CALL(mock, sum(2,-1)).WillRepeatedly(Return(1));
    EXPECT_EQ(1, check(2, -1, 1) );
    EXPECT_EQ(0, check(2, -1, 2) );
}

EXPECT_CALL(mock, sum(1,2)).WillRepeatedly(Return(3));

  • mockのsum(1,2)がコールされる度に3を返す。

EXPECT_CALL(mock, sum(2,-1)).WillRepeatedly(Return(1));

  • mockのsum(2,-1)がコールされる度に1を返す。

注意

これを実現するには、モックにする関数を関数ポインタに修正する必要があります。

つまり、テストの為に、既存の製品コードを修正する必要があります。

  • ここがネックで、テストの自動実行を設定するのが難しいです。

sum.c

#include "sum.h"

/*
int sum(int a, int b)

*/
int sum_imple(int a, int b) //★実装関数を名前変更
{
  return (a+b);
}

int (*sum)(int a, int b) = sum_imple; // ★sumを関数ポインタで定義して、実装関数のポインタで初期化

sum.h

#ifndef SUM_H_
#define SUM_H_

/*
extern int sum(int a, int b);

*/
extern int (*sum)(int a, int b);//★関数ポインタに変更

#endif //SUM_H_

テスト結果

■GoogleTestを開始しました
[==========] Running 4 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from SumTest
[ RUN      ] SumTest.add1
[ 初期化   ]
[ SetUp    ]
■SumTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.add1 (0 ms)
[ RUN      ] SumTest.minus1
[ 初期化   ]
[ SetUp    ]
■SumTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.minus1 (0 ms)
[----------] 2 tests from SumTest (0 ms total)

[----------] 2 tests from CheckTest
[ RUN      ] CheckTest.add1
[ 初期化   ]
[ SetUp    ]
■CheckTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] CheckTest.add1 (0 ms)
[ RUN      ] CheckTest.minus1
[ 初期化   ]
[ SetUp    ]
■CheckTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] CheckTest.minus1 (0 ms)
[----------] 2 tests from CheckTest (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 2 test cases ran. (0 ms total)
[  PASSED  ] 4 tests.

モック対象の関数sum(int a, int b) の引数に関係なく返す値を制御したい場合

以下の様にします。

_を使う。

using ::testing::_;

テストケース

TEST_F(CheckTest, add1)
{
    printf("■CheckTest 加算テスト\n");
    EXPECT_CALL(mock, sum(_,_)).WillRepeatedly(Return(3));
    EXPECT_EQ(1, check(2, 100, 3));
    EXPECT_EQ(0, check(2, 100, 4));
}

TEST_F(CheckTest, minus1)
{
    printf("■CheckTest 減算テスト\n");
    EXPECT_CALL(mock, sum(_,_)).WillRepeatedly(Return(1));
    EXPECT_EQ(1, check(2, -100, 1) );
    EXPECT_EQ(0, check(2, -100, 2) );
}
  • テストでは、sum(2, 100)とsum(2, -100)となる様な引数をcheck関数にセットしていますが、モックの振る舞いでは、sum関数は引数に関係なく必ず3又は1を返す様な設定です。

EXPECT_CALL(mock, sum(,)).WillRepeatedly(Return(3));

  • sum()の引数に関係なく必ず3を返す

EXPECT_CALL(mock, sum(,)).WillRepeatedly(Return(1));

  • sum()の引数に関係なく必ず1を返す

テスト結果

※モックが返す値は前回と変わらない為、テストはOKとなります。

(こででテストが通るのはイマイチと言われそうですが。。。)

■GoogleTestを開始しました
[==========] Running 4 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from SumTest
[ RUN      ] SumTest.add1
[ 初期化   ]
[ SetUp    ]
■SumTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.add1 (0 ms)
[ RUN      ] SumTest.minus1
[ 初期化   ]
[ SetUp    ]
■SumTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.minus1 (0 ms)
[----------] 2 tests from SumTest (0 ms total)

[----------] 2 tests from CheckTest
[ RUN      ] CheckTest.add1
[ 初期化   ]
[ SetUp    ]
■CheckTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] CheckTest.add1 (0 ms)
[ RUN      ] CheckTest.minus1
[ 初期化   ]
[ SetUp    ]
■CheckTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] CheckTest.minus1 (0 ms)
[----------] 2 tests from CheckTest (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 2 test cases ran. (0 ms total)
[  PASSED  ] 4 tests.

2回目にモックが返す値を変更する

2回目にモックが返す値を変更して、2回目のテストを失敗させてみます。

WillOnceを使います。

TEST_F(CheckTest, add1)
{
    printf("■CheckTest 加算テスト\n");
    EXPECT_CALL(mock, sum(_,_))
     .WillOnce(Return(3))
     .WillOnce(Return(4))            //★ 2回目の返す値を変更
     .WillRepeatedly(Return(3)); //★ 3回目以降はこちら
    EXPECT_EQ(1, check(2, 100, 3));
    EXPECT_EQ(0, check(2, 100, 4));  //★ このテストで失敗するはず
}

TEST_F(CheckTest, minus1)
{
    printf("■CheckTest 減算テスト\n");
    EXPECT_CALL(mock, sum(_,_))
     .WillOnce(Return(1))
     .WillOnce(Return(2))           //★ 2回目の返す値を変更
     .WillRepeatedly(Return(1)); //★ 3回目以降はこちら
    EXPECT_EQ(1, check(2, -100, 1) );
    EXPECT_EQ(0, check(2, -100, 2) ); //★ このテストで失敗するはず
}

テスト結果

※2回目のテストで失敗しているのがわかります。

■GoogleTestを開始しました
[==========] Running 4 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from SumTest
[ RUN      ] SumTest.add1
[ 初期化   ]
[ SetUp    ]
■SumTest 加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.add1 (0 ms)
[ RUN      ] SumTest.minus1
[ 初期化   ]
[ SetUp    ]
■SumTest 減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.minus1 (0 ms)
[----------] 2 tests from SumTest (0 ms total)

[----------] 2 tests from CheckTest
[ RUN      ] CheckTest.add1
[ 初期化   ]
[ SetUp    ]
■CheckTest 加算テスト
../src/CheckTest.cpp:120: Failure
Expected equality of these values:
  0
  check(2, 100, 4)
    Which is: 1
[ TearDown ]
[ 終了処理 ]
[  FAILED  ] CheckTest.add1 (0 ms)
[ RUN      ] CheckTest.minus1
[ 初期化   ]
[ SetUp    ]
■CheckTest 減算テスト
../src/CheckTest.cpp:131: Failure
Expected equality of these values:
  0
  check(2, -100, 2)
    Which is: 1
[ TearDown ]
[ 終了処理 ]
[  FAILED  ] CheckTest.minus1 (0 ms)
[----------] 2 tests from CheckTest (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 2 test cases ran. (0 ms total)
[  PASSED  ] 2 tests.
[  FAILED  ] 2 tests, listed below:
[  FAILED  ] CheckTest.add1
[  FAILED  ] CheckTest.minus1

 2 FAILED TESTS

#ifdefで対応する例

最後に、モック対象関数を関数ポインタに置き換える部分を、#ifdefで対応する例をあげつつ、 コードをひととおり記載します。

TestMain.cpp

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

int main(int argc, char** argv) {
    printf("■GoogleTestを開始しました\n");
  // 以下の行は,テスト開始前に Google Mock (と Google Test)
  // を初期化するために必ず実行する必要があります.
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}

common.h

#ifndef COMMON_H_
#define COMMON_H_

#define UNITTEST

#endif /* COMMON_H_ */

check.h

#ifndef CHECK_H
#define CHECK_H

extern int check(int a, int b, int c);

#endif //CHECK_H

check.c

#include "common.h"
#include "check.h"
#include "sum.h"

int check(int a, int b, int c)
{
  int result = 0;

  if ( sum(a,b) >= c ) 
  {
      result = 1;
  }
  else
  {
      result = 0;
  }

  return (result);
}

sum.h

#ifndef SUM_H_
#define SUM_H_

#ifdef UNITTEST
extern int (*sum)(int a, int b);//★関数ポインタに変更
#else
extern int sum(int a, int b);
#endif

#endif //SUM_H_

sum.c

#include "common.h"
#include "sum.h"

#ifdef UNITTEST
int sum_imple(int a, int b) //★実装関数を名前変更
#else
int sum(int a, int b)
#endif
{
  return (a+b);
}

#ifdef UNITTEST
int (*sum)(int a, int b) = sum_imple; // ★sumを関数ポインタで定義して、実装関数のポインタで初期化
#endif

CheckTest.cpp

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

extern "C" {
#include "common.h"
#include "check.h"
#include "sum.h"
}

namespace {

using ::testing::Return;
using ::testing::_;

// テスト対象となる関数 Sum のためのフィクスチャ
class SumTest : public ::testing::Test {
protected:
// 以降の関数で中身のないものは自由に削除できます.

    // コンストラクタ(初期化用)
    SumTest()
    {// テスト毎に実行される set-up をここに書きます.
        printf("[ 初期化   ]\n");
    }

    // デストラクタ(終了処理用)
    virtual ~SumTest()
    {// テスト毎に実行される,例外を投げない clean-up をここに書きます.
        printf("[ 終了処理 ]\n");

    }

    // コンストラクタとデストラクタでは不十分な場合.
    // 以下のメソッドを定義することができます:

    virtual void SetUp()
    {// このコードは,コンストラクタの直後(各テストの直前)に呼び出されます.
        printf("[ SetUp    ]\n");
    }

    virtual void TearDown()
    {// このコードは,各テストの直後(デストラクタの直前)に呼び出されます.
        printf("[ TearDown ]\n");

    }
// ここで宣言されるオブジェクトは,テストケース内の全てのテストで利用できます.
};

TEST_F(SumTest, add1)
{
    printf("■SumTest 加算テスト\n");
    EXPECT_EQ(3, sum(1, 2));
}

TEST_F(SumTest, minus1)
{
    printf("■SumTest 減算テスト\n");
    EXPECT_EQ(1, sum(2, -1) );
}

/* Mocksum モッククラス */
class MockSum
{
public:
  MOCK_METHOD2(sum, int(int a, int b) );
}mock;

/* モック関数 */
int Mock_sum(int a, int b)
{
    return mock.sum(a, b);
}

// テスト対象となる関数 check のためのフィクスチャ
class CheckTest : public ::testing::Test {
protected:
// 以降の関数で中身のないものは自由に削除できます.
    int (*saved_sum)(int a, int b);

    // コンストラクタ(初期化用)
    CheckTest()
    {// テスト毎に実行される set-up をここに書きます.
        printf("[ 初期化   ]\n");
    }

    // デストラクタ(終了処理用)
    virtual ~CheckTest()
    {// テスト毎に実行される,例外を投げない clean-up をここに書きます.
        printf("[ 終了処理 ]\n");

    }

    // コンストラクタとデストラクタでは不十分な場合.
    // 以下のメソッドを定義することができます:

    virtual void SetUp()
    {// このコードは,コンストラクタの直後(各テストの直前)に呼び出されます.
        printf("[ SetUp    ]\n");
        saved_sum = sum;
        sum = Mock_sum;
    }

    virtual void TearDown()
    {// このコードは,各テストの直後(デストラクタの直前)に呼び出されます.
        printf("[ TearDown ]\n");
        sum = saved_sum;
    }
// ここで宣言されるオブジェクトは,テストケース内の全てのテストで利用できます.
};

TEST_F(CheckTest, add1)
{
    printf("■CheckTest 加算テスト\n");
    EXPECT_CALL(mock, sum(1,2)).WillRepeatedly(Return(3));
    EXPECT_EQ(1, check(1, 2, 3));
    EXPECT_EQ(0, check(1, 2, 4));
}

TEST_F(CheckTest, minus1)
{
    printf("■CheckTest 減算テスト\n");
    EXPECT_CALL(mock, sum(2,-1)).WillRepeatedly(Return(1));
    EXPECT_EQ(1, check(2, -1, 1) );
    EXPECT_EQ(0, check(2, -1, 2) );
}
}//namespace

組込C言語でUnitTest 4 簡単なテスト

GoogleTestで簡単なテストです。

テスト対象の関数

sum.c

#include "sum.h"

int sum(int a, int b)
{
  return (a+b);
}

sum.h

#ifndef SUM_H_
#define SUM_H_

extern int sum(int a, int b);

#endif //SUM_H_

テスト用のMain関数

Main関数ファイルを別に作成しておきます。

TestMain.cpp

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

int main(int argc, char** argv) {
    printf("■GoogleTestを開始しました\n");
  // 以下の行は,テスト開始前に Google Mock (と Google Test)
  // を初期化するために必ず実行する必要があります.
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}

テストコード

SumTestという名前のテストケースに、

・add1という名前の加算テストと

・minus1という名前の減算テスト

を書きました。

それぞれ、

1+2=3

2-1=1

をテストする簡単なテストを書きました。

★テストする関数sumがC++のリンカーから見える様に、extern "C" でヘッダファイルのインクルードを囲みます。

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

extern "C" {
#include "sum.h"
}

namespace {

// テスト対象となる関数 Sum のためのフィクスチャ
class SumTest : public ::testing::Test {
protected:
// 以降の関数で中身のないものは自由に削除できます.

    // コンストラクタ(初期化用)
    SumTest()
    {// テスト毎に実行される set-up をここに書きます.
        printf("[ 初期化   ]\n");
    }

    // デストラクタ(終了処理用)
    virtual ~SumTest()
    {// テスト毎に実行される,例外を投げない clean-up をここに書きます.
        printf("[ 終了処理 ]\n");

    }

    // コンストラクタとデストラクタでは不十分な場合.
    // 以下のメソッドを定義することができます:

    virtual void SetUp()
    {// このコードは,コンストラクタの直後(各テストの直前)に呼び出されます.
        printf("[ SetUp    ]\n");
    }

    virtual void TearDown()
    {// このコードは,各テストの直後(デストラクタの直前)に呼び出されます.
        printf("[ TearDown ]\n");

    }
// ここで宣言されるオブジェクトは,テストケース内の全てのテストで利用できます.
};

TEST_F(SumTest, add1)
{
    printf("■加算テスト\n");
    EXPECT_EQ(3, sum(1, 2) );
}

TEST_F(SumTest, minus1)
{
    printf("■減算テスト\n");
    EXPECT_EQ(1, sum(2, -1) );
}

}// namespace

テスト結果

■GoogleTestを開始しました
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from SumTest
[ RUN      ] SumTest.add1
[ 初期化   ]
[ SetUp    ]
■加算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.add1 (0 ms)
[ RUN      ] SumTest.minus1
[ 初期化   ]
[ SetUp    ]
■減算テスト
[ TearDown ]
[ 終了処理 ]
[       OK ] SumTest.minus1 (0 ms)
[----------] 2 tests from SumTest (0 ms total)

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

わざと失敗させた結果はこちら

テストコード

TEST_F(SumTest, add1)
{
    printf("■加算テスト\n");
    EXPECT_EQ(3, sum(1, 3));
}

TEST_F(SumTest, minus1)
{
    printf("■減算テスト\n");
    EXPECT_EQ(1, sum(2, -3) );
}

テスト結果

■GoogleTestを開始しました
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from SumTest
[ RUN      ] SumTest.add1
[ 初期化   ]
[ SetUp    ]
■加算テスト
../src/CheckTest.cpp:49: Failure
Expected equality of these values:
  3
  sum(1, 3)
    Which is: 4
[ TearDown ]
[ 終了処理 ]
[  FAILED  ] SumTest.add1 (0 ms)
[ RUN      ] SumTest.minus1
[ 初期化   ]
[ SetUp    ]
■減算テスト
../src/CheckTest.cpp:55: Failure
Expected equality of these values:
  1
  sum(2, -3)
    Which is: -1
[ TearDown ]
[ 終了処理 ]
[  FAILED  ] SumTest.minus1 (0 ms)
[----------] 2 tests from SumTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (0 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 2 tests, listed below:
[  FAILED  ] SumTest.add1
[  FAILED  ] SumTest.minus1

 2 FAILED TESTS

組込C言語でUnitTest 3 リンク集

参考ページ

ここでは、参考リンクを貼り付けします。

TDDと言えば、和田卓人さん

gihyo.jp

組込みソフトウェア開発 ノウハウ集

GoogleTest の使用を推奨する理由も少し述べられている

組込みソフトウェア開発 ノウハウ集 — 組込みソフトウェア開発 ノウハウ集 0.5.0 ドキュメント

テスト駆動開発による組み込みプログラミングをgoogletestでやる

TDD本は CppUTestですが、それをGoogletestでも実施しています。

qiita.com

テスト駆動開発による組み込みプログラミングのつどい

legoboku.blogspot.com

Google Test ドキュメント日本語訳

Google Test — Google Test ドキュメント日本語訳

Google Mock ドキュメント日本語訳

Google Mock — Google Mock ドキュメント日本語訳

組込C言語でUnitTest 2

前回のつづきから

sioaji2012.hatenablog.com

普段は、下記の本を参考にTDD(テスト駆動開発)を勉強しています。

テスト駆動開発による組み込みプログラミングC言語オブジェクト指向で学ぶアジャイルな設計」:Amazon CAPTCHA

まだ半分くらいしか読んでいません。(^_^*)

Facebook組込みTDD勉強会にも参加させて頂いています。

ツールはOSSののCppUTestです。TDDをやる理由とやり方とソースが載っています。

勉強半ばで、なかなかOUTPUTは出来ない状況です。

googletestのセットアップ

Googletestのソースを取得します

GitHub - google/googletest: Google Test

MinGWをいれる

インストールは、まずは、WindowsMinGW (またはCygwinでもいいですが) を入れました。 (インストール後、MinGWのbinフォルダと Msysのbinフォルダのpathが通っている事を確認しておく)

osdn.net

MinGWにMsysというコマンドライン実行ツールみたいのがついてるので、msysを起動してそこから googletestのソースをビルドしました。

googletestのビルド方法はこちらにありますが、

googletest/README.md at master · google/googletest · GitHub

googlemockを使うと思うので、こちらはスキップしてgooglemockのビルド方法を実施した方が良いと思います。

(ググった先で、2つを別々にビルドしてうまく動かなかった人がいたみたいなので)

GoogleMockのビルド(Googletestも同時ビルド)

googletest/README.md at master · google/googletest · GitHub

上記ページの真ん中くらいのコマンドを実行します。

C直下にソースを置きました。(フォルダ例)

/c/googletest/googletest
/c/googletest/googlemock
・・・・

msysを起動して、googlemockのフォルダで実行

/c/googletest/googlemock
$ autoreconf -fvi

これをやれ。と書いてあるので、

g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
    -isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \
    -pthread -c ${GTEST_DIR}/src/gtest-all.cc
g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
    -isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \
    -pthread -c ${GMOCK_DIR}/src/gmock-all.cc
ar -rv libgmock.a gtest-all.o gmock-all.o

こんな感じに置き換えて順番に実施

g++ -isystem /c/googletest/googletest/include -I /c/googletest/googletest \
-pthread -c /c/googletest/googletest/src/gtest-all.cc
ar -rv libgtest.a gtest-all.o

g++ -isystem /c/googletest/googletest/include -I/c/googletest/googlemock \
-isystem /c/googletest/googlemock/include -I/c/googletest/googlemock \
-pthread -c /c/googletest/googletest/src/gtest-all.cc
g++ -isystem /c/googletest/googletest/include -I /c/googletest/googletest \
-isystem /c/googletest/googlemock/include -I/c/googletest/googlemock \
-pthread -c /c/googletest/googlemock/src/gmock-all.cc
ar -rv libgmock.a gtest-all.o gmock-all.o

最後のところ。オブジェクトファイルが出来た。

$ ar -rv libgmock.a gtest-all.o gmock-all.o
a - gtest-all.o
a - gmock-all.o

フォルダ内に、リンク用のオブジェクトファイル

  • libgtest.a
  • libgmock.a

ができてると思います。

実際のテストコードをビルドするコマンド

上記ページには、こうやってビルドして。と書いてありますが、

g++ -isystem ${GTEST_DIR}/include -isystem ${GMOCK_DIR}/include \
    -pthread path/to/your_test.cc libgmock.a -o your_test

eclipseから実行したかったので、こちらのページを参考にして実施しました。

googletestのみの説明ですが、googlemockも同じ様に設定します。

(-pthread の指定がされていないかもしれないのが気になる)

GoogleTest Installation and Use in Eclipse/C++ Instructions

やっていることは、だいたいこんな感じ。


  • MINGWコンパイラに使う
  • GCC C++ Compiler の Miscellaneous の、その他フラグに、 -std=gnu++11 を最後に追加。
  • GCC C++ Compiler の Includes に、googleのソースのincludeフォルダを指定する
  • MinGW C++ Linker の Libraries に、パスを指定(上記ビルドして作ったリンク用オブジェクトファイル)、名前を設定。(libと.aを除いた部分⇛gtest と gmock)

あとは、サンプルコードで試してみればテスト出来る環境になっている事が確認出来ます。

eclipseでやるときの注意

eclipseにも、mingwが入っています。

Windowsに別途入れたMinGWeclipseminGWとではバージョンが違うので、そのままだとうまくリンク出来ないので、 eclipsemingwを使わない様にする必要があるかもしれません。

方法は、eclipseのCC C++ Compiler のビルドタブあたりに、パスを設定するとことがあるので、 eclipse/mingw32/binみたいなパスの登録を削除します。

はじめから、googletestのビルドをeclipseでビルドできれば、MINGW自体を入れる必要もないのですが・・・(出来るか不明)


つづきは、また今度

組込C言語でUnitTest 1

組込開発C言語でテストコード書きたい

普段は、組み込みファーム開発で C言語 しかプログラムしていません。

今のところ、TDD(テスト駆動開発をやりたいわけではありません。
というか出来ないです。。
(やってみたいですが・・・)

私の会社では、シミュレータを自前で開発したり、実行ログの自動解析ツールを開発したりして、 動的なシステムテストが主流になっているので、 テストコードを書くという文化はありませんし、メリットを感じられていない様です。

実際に、『テストコード書ける様に環境を作っていきます!』 と宣言した時も、一部の方から猛反撃をくらいました。

  • ほんとに効果あるの?
  • テストコードを書くにあたっての費用対効果を説明してから
    テストコード書きましょう。って言ってくださいよ!
  • 何か効果あるらしいから、やってみようは拒否する!
  • 効果があるかどうかわからん物の環境構築って、、
    それ自体無駄だからやめた方がいいんじゃないの!

・・・怖っ・・・

一応は、『テストする事についての一般的な考え』を説明してましたが。。。

後工程での不具合だと

  • 原因にたどり着くまで時間がかかる
  • 大昔の実装なので当時の資料を掘り起こす時間ロス
  • システムとして既に作り込まれているので、修正による影響範囲とか確認項目が莫大
  • 市場に不具合が出回る

前工程での不具合になると

逆の事が言える

  • 修正したらすぐテスト失敗を報告してくれるので、原因を教えてくれてるに近い。
  • 実装したばかりなので、誰がいつ何の為に実装した?とか資料集めなどいらない事が多い。
  • 修正による影響範囲とか確認項目は、たぶんさっき修正したのとそんなに変わらない。
  • 市場に不具合が出回る前に見つかった!

継続的インテグレーション

  • 昔書いたテストが不具合を教えてくれる。自分の修正箇所が他の不具合を誘発する事に気がつく。

その他

  • テストしやすいモジュール化された良いコードが出来る。
  • リファクタリングしやすくなる (上記につながる )
  • システムでは発生させるのが難しいパターンを試せる。
  • 不具合の再現確認が出来る。修正の効果確認も同様、
  • なんか、安心だ!(モチベーション大事)

デメリット

  • テストコード書く時間が増える
  • そもそもテストコード書き方の勉強が必要。
  • ハード依存の部分や、システムテストが難しいので出来ない。
  • 仕様変更や不具合修正のたびにテストコードのメンテが必要。
  • テストコード自体の信頼性が気になる(やりたいテスト。目論見のテストをしてくれているのか)

それでもやるしかない

上からおりてきた仕事ですから、やらないという選択肢は簡単ではないですし。

ただ、只今の状況は、『非常にきびしー』です。

理由

  • レガシーコード*1だから。
    テストは当然ないですが、よくわからない関数が多いのでテストが書けない。
    コード流用することは美。リファクタリングするのは悪。で、レガシーコードが蔓延しています。
    (そもそもソース構造をまとめた資料もないし)
  • デメリットをくつがえす実例を示さないといけない。(費用対効果)
    テスト出来るところを見つけて。効果を積み重ねる必要がある。でもそこまでコツコツとやらせてくれるかどうか。・・・というと駄目だろうな。
  • テストの為にオリジナルのソースに手を加えたくない。
    モックが必要になった場合に、オリジナルのソースをいじらずに出来るか。
    C言語の壁が待っていそう。

一応、

選んだツールは、OSSの GoogleTest

本当は、CppUTest が組み込み用で広く使われているので、そちらが良かったかもしれませんが、

選んだ理由の概略は、モックのコード行数が少なく済むみたいなのと、Googleさんも自分たちでつかってるんだろうという事です。


最後は、商用ツール買えば楽にテストコード書けますよ。になるかもしれない。(会社ですし)

会社でやったことは、ここでは書けませんが、

redmineプラグイン開発のお勉強を保留して自分の時間を使ってしまいましたし、

(これはこれでグレーですが・・・。自己啓発ということで。。。)

少し覚え書きな感じになりますが、自宅で検討した内容を、あと少し書き留めていこうと思います。

つづきは、次回へ。


*1:一般的には『理解しづらい・変更しにくいコード』TDD本?では『テストの無いコード』だと思います

Redmineプラグインもくもく 山梨 #007回 プラグイン作成試し

前回、

Rubyの記述の意味がわかりませんでした

これ。

module RedmineIine
  class Hook < Redmine::Hook::ViewListener
    def view_layouts_base_content(context = {})
      controller = context[:controller]
      if controller.controller_name == 'issues' && controller.action_name == 'show'
        controller.render partial: 'redmine_iine/hooks/view_layouts_base_content'
      end
    end
  end
end

少しだけ、勉強しようと思いますが・・・。

(本来は体系的な勉強しなければいけないですが・・・すみません。。。)

モジュール - Ruby入門

Redmineプラグイン作成でHookを使ってみる - torutkの日記

上記ページからの抜粋になりますが、調べた結果を書きます。

Rubyではクラスという概念に似たものとしてモジュールという概念があります。

・モジュールはクラスと同じくメソッドを定義する事が出来ます。
・クラス変数に相当するものはモジュールにはありませんが、
 定数は定義する事が出来ます。
・クラスはクラスからオブジェクトを作成することが出来ますが、
 モジュールでは作成することは出来ません。

フックへ登録するコールバックの定義

 まず、フックのコールバックを実装するクラスを定義します。
 このクラスは、Redmine::Hook::ViewListenerクラスを継承します。
 クラス定義を記述するファイルは、プラグインのlib下に置きます

lib/redmine_iine/hook.rb

module RedmineIine ★モジュール名
  class Hook < Redmine::Hook::ViewListener ★クラスを継承
    def view_layouts_base_content(context = {}) ★クラスと同じメソッド
      controller = context[:controller]
      if controller.controller_name == 'issues' && controller.action_name == 'show'
        controller.render partial: 'redmine_iine/hooks/view_layouts_base_content' ★部分テンプレート
      end
    end
  end
end

・モジュールの利用方法としては、
 「モジュール名.メソッド名」の形式で関数のように実行するか、
 他のクラスの中にインクルードして利用することが出来ます。

Redmineプラグイン作成の情報を調べていると、
 moduleを定義している例と定義していない例を見かけますが
 (定義していないものが多い感触)、
 書籍「Redmine Plugin Extension and Development」では
 moduleを導入しているのでそれに倣っています。
 moduleにはプラグイン名をPascalケースにした文字列を指定しています。

・クラスの中身ですが、フック名と同じ名前のメソッドを定義して、
 その中で文字列(HTML)を作成しreturnするサンプルをよく見かけますが、
 書籍ではrender_onでフック名と部分テンプレートを指定している。

部分テンプレートは、ここで指定した名前の先頭にアンダースコアを付け、
 拡張子を.erbとしたファイルに記述します
 (拡張子は、.html.erbなどでも可)。
 ファイルの場所はプラグインのapp/views/redmine_iine/hooks/の下に置きました。

app/views/redmine_iine/hooks/_view_layouts_base_content.html.erb

<%= link_to 'いいね!', '#', class: 'redmine-iine', style: 'display: none;' %>

<script>
  $('.redmine-iine').on('click', function(e) {
    e.preventDefault();
    alert('いいね!');
  });

  $(function() {
    $('.redmine-iine').prependTo('#content .contextual').show();
  });
</script>

フックへ登録(init.rb)

init.rbに次を記述します。

require_dependency 'redmine_iine/hook'

libしたのlib/redmine_iine/hook.rbが呼び込まれます。

部分テンプレートを適用する条件

controller = context[:controller]
if controller.controller_name == 'issues' && controller.action_name == 'show'
  controller.render partial: 'redmine_iine/hooks/view_layouts_base_content' ★部分テンプレート
end

実装したい画面のコントローラ名:'issues'
アクション名: 'show'
これらを探すときは、
・実際にRedmine操作して、ログから追うのが早い。
・あとは、routesを読むか。
(ハンズオンより)


今回は、以上です。
わかった様な気になっているだけですが、次に進もうと思います。
只今、体調が絶不調中。。。会社も休みがちで、もくもくもあまり進められませんでした。


Redmineプラグインもくもく 山梨 #006回 プラグイン作成試し

前回、init.rb を書いただけという状態でした。 今回は、こちら。

チケット詳細ページにいいねを表示する。

既存のviewに何か要素を追加するときは、hook + jsで!

公式ページ hookリスト

www.redmine.org

既存画面に何か追加したい場合に、
上記のHooks List の中にあるView Hooksを使って実装していく。

パーシャル(Viewの断片ページ)を呼ぶ。

View Hooksでerbを挿入することになるが、
表示したいhtmlをそのまま挿入とせず、Javascriptで要素を差し込むような感じ。
Redmine本体のビューがとても変わりやすい為、JavaScriptで埋め込んだほうが後で柔軟に対応できる。

■app/views/redmine_iine/hooks/_view_layouts_base_content.html.erb
・'いいね!'のリンクにclass 'redmine-iine'を設定して、クラスで要素を指定。
・リンクが押されたらアラート表示。
・リンクは、'#content .contextual'要素の手前に追加表示する。

<%= link_to 'いいね!', '#', class: 'redmine-iine', style: 'display: none;' %>

<script>
  $('.redmine-iine').on('click', function(e) {
    e.preventDefault();
    alert('いいね!');
  });

  $(function() {
    $('.redmine-iine').prependTo('#content .contextual').show();
  });
</script>

汎用的なhookポイントを使う

RedmineのHooksでほしい位置に挿入できるようなHooksがないことがある。
(チケットの下に差し込むHookはあっても、上に差し込むのがなかったりなど)

Hookを上記ページからがんばって探すより、
汎用的に使える view_layouts_base_content で差し込んじゃう方を選ぶ感じ。

■lib/redmine_iine/hook.rb
・view_layouts_base_contentのHookを使う。
・html のbody のボトムに差し込まれる
・ここにおいて、(disply: none)、JSで置き換える。

module RedmineIine
  class Hook < Redmine::Hook::ViewListener
    def view_layouts_base_content(context = {})
      controller = context[:controller]
      if controller.controller_name == 'issues' && controller.action_name == 'show'
        controller.render partial: 'redmine_iine/hooks/view_layouts_base_content'
      end
    end
  end
end

■init.rb

require_dependency 'redmine_iine/hook'

Redmine::Plugin.register :redmine_iine do
  name 'Redmine いいね!Plugin'
  author 'sioaji'
  version '0.0.1'
end

実装したい画面のコントローラとアクション名を探すときは

実際にRedmine操作して、ログから追うのが早い。
あとは、routesを読むか。

ここまでの結果

表示した "いいね!" を押した場面。

f:id:sioaji2012:20180606211156p:plain


今日は、ここまで。
やっぱり、途中のフック指定するところが、ruby on rails な感じで
勉強しないとわからないので、お勉強しなければならない。…ですね。(^_^;)