🧪

GoogleTest/gMockを用いたC++ユニットテストの導入ガイド

2024/02/27に公開

はじめに

Turing UXチームでソフトウェアエンジニアをしています佐々木 (kento_sasaki1) です。Turingは「We Overtake Tesla」をミッションに完全自動運転EVメーカーを目指すスタートアップです。
UXチームでは車載ソフトウェアの開発を行っており、ユーザーインターフェースからハードウェア抽象化レイヤに至るまで幅広い範囲を担当しています。

本記事では、ハードウェア抽象化レイヤ (HAL) のC++実装に焦点を当て、C++テストフレームワークであるGoogleTest, gMockを用いたユニットテストの導入方法について紹介します。さらに、実践編として外部の非仮想メソッドをモックする方法について紹介します。

GoogleTest

GoogleTest (正式名称 : Google C++ Testing Framework)は、Googleが提供するユニットテストフレームワークです。

  1. C++環境構築
    C++の実行環境が手元にない場合は、以下のコマンドを実行してコンパイラをインストールしてください。
sudo apt-get update
sudo apt-get install build-essential gdb
g++ --version
  1. GoogleTestのインストール
    GoogleTestは以下のようにビルドし、インストールしてください。
sudo apt-get install cmake

git clone https://github.com/google/googletest.git
cd googletest
mkdir build
cd build
cmake ..
make
sudo make install
  1. GoogleTestを用いたテストケース
    簡単なテストケースを書いてみましょう。
gtest.cpp
#include <gtest/gtest.h>

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

TEST(TestCase, sum){
    EXPECT_EQ(2, sum(1, 1));
}
  1. テストの実行
    テストスクリプトをコンパイルして、テストを実行します。
$ g++ gtest.cpp -o gtest -lgtest -lgtest_main -pthread
$ ./gtest

//実行結果
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from TestCase
[ RUN      ] TestCase.sum
[       OK ] TestCase.sum (0 ms)
[----------] 1 test from TestCase (0 ms total)

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

PASSEDが表示されていれば、テストが成功しています。

先ほど実装したテストケースは以下のフォーマットに則っています。GoogleTestでは、<gtest/gtest.h>インクルードしてTEST()マクロを用いてテストケースを作成します。第1引数がテストケース名、第2引数がテスト名です。

TEST(TestSuiteName, TestName) {
  ... test body ...
}

EXPECT_EQマクロはGoogleTestのアサーションの一つであり、2つの値 (expected, actual)を比較するときに用います。第一引数は期待される値(expected)、第二引数は実際の値(actual)です。EXPECT_*はテストが失敗した場合でも、その後のテストが実行が継続されます。一方、ASSERT_*マクロはエラーの場合に処理が終了します。ASSERT_*は、特定の条件が満たされない場合に後続のテストが意味をなさない場面で使用されます。

数値の比較は、以下のアサーションを用います。

条件 アサーション
a == b EXPECT_EQ(a, b)
a != b EXPECT_NE(a, b)
a < b EXPECT_LT(a, b)
a <= b EXPECT_LE(a, b)
a > b EXPECT_GT(a, b)
a => b EXPECT_GE(a, b)

文字列の比較は、以下のアサーションを用います。

条件 アサーション
2つの C 文字列の内容が等しい EXPECT_STREQ(expected_str, actual_str);
2つの C 文字列の内容が等しくない EXPECT_STRNE(str1, str2);
大文字小文字を無視した場合,2つの C 文字列の内容が等しい EXPECT_STRCASEEQ(expected_str, actual_str);
大文字小文字を無視した場合,2つの C 文字列の内容が等しくない EXPECT_STRCASENE(str1, str2);

true/false条件はEXPECT_TRUE(condition), EXPECT_FALSEを用います。

条件 アサーション
condition が true EXPECT_TRUE(condition);
condition が false EXPECT_FALSE(condition);

そのほか、アサーションの詳細はGoogleTestドキュメントのアサーションリファレンスを参照してください。基本的な事項については以上です。次にモックを用いたテストの実行方法を確認してみましょう。

gMock

gMockは、Googleが開発したモック用のフレームワークです。テスト対象のコードが依存しているコンポーネント(例えば、データベースアクセスやネットワーク通信など)をモックすることでテストを単純化し、実行速度を向上させ、外部環境に依存せずにテストを行えるようになります。

  1. gMockのインストール
    以下のコマンドを実行し、Google Mockをインストールしてください。
sudo apt-get update
sudo apt-get -y install google-mock
  1. gMockを用いたテストケース
    ここでは、データベースをモックすることを例として考えてみます。
MyApp.h
class MyApp {
   public:
    MyApp(MockDatabaseAccess& db) : db_(db) {}
    void AddNewCustomer(const std::string& name, const std::string& email) { db_.AddCustomer(name, email); }
    std::vector<std::string> GetCustomers() { return db_.GetCustomers(); }
   private:
    MockDatabaseAccess& db_;
};
MyApp_test.cpp
#include <string>
#include <vector>

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

#include MyApp.h

class MockDatabaseAccess {
   public:
    MOCK_METHOD(void, AddCustomer, (const std::string& name, const std::string& email), ());
    MOCK_METHOD(std::vector<std::string>, GetCustomers, (), ());
};

TEST(MyAppTest, CanAddNewCustomer) {
    MockDatabaseAccess mockDb;
    MyApp app(mockDb);

    EXPECT_CALL(mockDb, AddCustomer("Alan Turing", "alanturing@example.com")).Times(1);

    app.AddNewCustomer("Alan Turing", "alanturing@example.com");
}

TEST(MyAppTest, CanGetCustomers) {
    MockDatabaseAccess mockDb;
    MyApp app(mockDb);

    std::vector<std::string> expectedCustomers = {"Alan Turing", "Elon Mask"};
    EXPECT_CALL(mockDb, GetCustomers()).WillOnce(::testing::Return(expectedCustomers));

    auto actualCustomers = app.GetCustomers();

    EXPECT_EQ(expectedCustomers, actualCustomers);
}
  1. テストの実行
g++ gmock.cpp -lgmock -lgtest -lgtest_main -pthread -o gmock

このように、gMockを用いることで外部依存がある場合においてもユニットテストを行うことができます。次に実践編として、外部の非仮想メソッドをモックする方法について紹介します。

実践編:外部の非仮想メソッドをモックする

ソフトウェアアーキテクチャにおいて外部クラスに対して直接依存せず、インタフェースを介することで外部クラスに依存し過ぎないことを目指します。ここでは、外部クラスの非仮想メソッドをモックし、インタフェースを介して外部クラスにアクセスします。これにより、
1) gMockで非仮想メソッドを持つクラスをモック可能にする
2) 外部クラスと境界を持ち、過度に依存しない
3) 外部ハードウェア(車両)に依存しない
ことを実現します。

想定するアーキテクチャを下図に示します。車両とAndroidがCAN通信を行うための一部処理を対象としています。
アーキテクチャ図

  • CanMessageProcessor : CAN通信(車載ネットワークで一般的に用いられるプロトコル)のメッセージを処理するクラス。
    • createVehiclePropValues : CAN信号をアプリケーションが解釈できるプロパティに変換するメソッド。
  • VehiclePropertyStore : 車両から送られるCAN信号を保存し、他のメソッドに提供する外部クラス。
    • readValuesForProperty : Storeに保存している値を呼び出すメソッド。

それでは、ユニットテストを行うためにどのようにすればよいか考えてみましょう。現状では、VehiclePropertyStoreは車両とCAN通信を行う、readValuesForPropertyが非仮想メソッドである、これら理由から依存関係をそのままテストに組み込むことが困難です。

そこで、VehiclePropertyStoreをモックすることにします。そのために、まずはIVehicleStateMangerインタフェースを作成しましょう。インタフェースを介した実装がVehiclePropertyStoreにアクセスすることで、ユニットテストを行うときは実装部分をモックに置き換えることを可能にします。
IVehicleStateMangerインタフェースを追加

IVehicleStateManger.h
class IVehicleStateManager {
  public:
    virtual std::vector<VehiclePropValue> readValues(int32_t propId) const = 0;
    virtual ~IVehicleStateManager() = default;
};
VehicleStateManagerImpl.h
class VehicleStateManagerImpl : public IVehicleStateManager {
  public:
    std::vector<VehiclePropValue> readValues(int32_t propId) const override {
        return mVehiclePropStore.readValuesForProperty(propId);
    }
    VehicleStateManagerImpl(VehiclePropertyStore& propStore) : mVehiclePropStore(propStore) {}

  private:
    VehiclePropertyStore& mVehiclePropStore;
};

インタフェースを作成するときのポイントは、readValuesを仮想メソッドにすることです。VehiclePropertyStoreクラスのreadValuesForPropertyは非仮想メソッドであり、gMockでは直接的にモックできません。仮想メソッドにすることで、gMockを用いてVehiclePropertyStoreをモックできるようになります。

以上で準備は終わりです。あとは、以下のようにMockVehiclePropertyStoreクラスを用意すれば、ユニットテストが実行できます。
MockVehiclePropertyStore

CanMessageProcessor_test.cpp
class MockVehiclePropertyStore : public IVehicleStateManager {
  public:
    MOCK_METHOD(std::vector<VehiclePropValue>, readValues, (int32_t propId), (const, override));
};

class HvacAcOnTest : public ::testing::Test {};

TEST_F(HvacAcOnTest, HvacAcOnTestForFirstSet) {
    int32_t storedValue = 0;
    int32_t inputValue = 1;
    uint8_t expectedPayload = 0x01;

    //
    MockVehiclePropertyStore mockPropStore;
    //

    EXPECT_EQ(expected, actual);
}

テストケースではmockPropertyStoreクラスからをインスタンスを生成して、Storeの値をパラメータとして与えることで想定する任意のStore値に対するユニットテストが実行できます。

まとめ

本記事では、GoogleTest, gMockの導入方法と実践編として非仮想メソッドをモックする方法を紹介しました。外部ライブラリやAPI通信などの依存関係があるC++ユニットテストにおいて、参考になれば幸いです。

お知らせ

「Turing Semiconductor/AI Day」を開催します。「Turing Semiconductor/AI Day」では、車載可能な生成AIアクセラレーター半導体「Hummingbird」やマルチモーダル生成AIの開発進捗および今後のロードマップ発表などを通じて、チューリングが取り組む半導体 × 生成AI × 自動運転の最先端をお届けします。詳細は以下のリンクをご覧ください。参加費無料ですので、ご興味ある方はぜひお申し込みください。
https://prtimes.jp/main/html/rd/p/000000044.000098132.html

Tech Blog - Turing

Discussion