C++ テスト(エージェントスキル)
GoogleTest/GoogleMock と CMake/CTest を使用した、最新 C++(C++17/20)向けのエージェント特化型テストワークフロー。
使用するタイミング
- 新しい C++ テストを作成する、または既存のテストを修正する場合
- C++ コンポーネントの単体/結合テストカバレッジを設計する場合
- テストカバレッジ、CI ゲーティング、またはリグレッション保護を追加する場合
- 一貫した実行のための CMake/CTest ワークフローを設定する場合
- テストの失敗やフレーキーな動作を調査する場合
- メモリ/競合診断用のサニタイザーを有効にする場合
使用すべきでないタイミング
- テスト変更を伴わない新しいプロダクト機能の実装
- テストカバレッジや失敗に関係のない大規模リファクタリング
- テストリグレッションで検証しないパフォーマンスチューニング
- C++ 以外のプロジェクト、またはテスト以外のタスク
コアコンセプト
- TDD ループ: レッド → グリーン → リファクタ(テストファースト、最小の修正、その後クリーンアップ)。
- 分離: グローバル状態よりも依存性注入とフェイクを優先。
- テストレイアウト:
tests/unit、tests/integration、tests/testdata。 - モック vs フェイク: モックはインタラクション用、フェイクはステートフルな振る舞い用。
- CTest ディスカバリ: 安定したテスト発見には
gtest_discover_tests()を使用。 - CI シグナル: 最初にサブセットを実行し、その後
--output-on-failure付きでフルスイートを実行。
TDD ワークフロー
RED → GREEN → REFACTOR ループに従ってください:
- RED: 新しい振る舞いを捉える失敗するテストを書く
- GREEN: テストを通過させる最小の変更を実装する
- REFACTOR: テストがグリーンのままクリーンアップする
// tests/add_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // Provided by production code.
TEST(AddTest, AddsTwoNumbers) { // RED
EXPECT_EQ(Add(2, 3), 5);
}
// src/add.cpp
int Add(int a, int b) { // GREEN
return a + b;
}
// REFACTOR: simplify/rename once tests pass
コード例
基本的な単体テスト(gtest)
// tests/calculator_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // Provided by production code.
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
フィクスチャ(gtest)
// tests/user_store_test.cpp
// Pseudocode stub: replace UserStore/User with project types.
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
struct User { std::string name; };
class UserStore {
public:
explicit UserStore(std::string /*path*/) {}
void Seed(std::initializer_list<User> /*users*/) {}
std::optional<User> Find(const std::string &/*name*/) { return User{"alice"}; }
};
class UserStoreTest : public ::testing::Test {
protected:
void SetUp() override {
store = std::make_unique<UserStore>(":memory:");
store->Seed({{"alice"}, {"bob"}});
}
std::unique_ptr<UserStore> store;
};
TEST_F(UserStoreTest, FindsExistingUser) {
auto user = store->Find("alice");
ASSERT_TRUE(user.has_value());
EXPECT_EQ(user->name, "alice");
}
モック(gmock)
// tests/notifier_test.cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
class Notifier {
public:
virtual ~Notifier() = default;
virtual void Send(const std::string &message) = 0;
};
class MockNotifier : public Notifier {
public:
MOCK_METHOD(void, Send, (const std::string &message), (override));
};
class Service {
public:
explicit Service(Notifier ¬ifier) : notifier_(notifier) {}
void Publish(const std::string &message) { notifier_.Send(message); }
private:
Notifier ¬ifier_;
};
TEST(ServiceTest, SendsNotifications) {
MockNotifier notifier;
Service service(notifier);
EXPECT_CALL(notifier, Send("hello")).Times(1);
service.Publish("hello");
}
CMake/CTest クイックスタート
# CMakeLists.txt (excerpt)
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Prefer project-locked versions. If using a tag, use a pinned version per project policy.
set(GTEST_VERSION v1.17.0) # Adjust to project policy.
FetchContent_Declare(
googletest
# Google Test framework (official repository)
URL https://github.com/google/googletest/archive/refs/tags/${GTEST_VERSION}.zip
)
FetchContent_MakeAvailable(googletest)
add_executable(example_tests
tests/calculator_test.cpp
src/calculator.cpp
)
target_link_libraries(example_tests GTest::gtest GTest::gmock GTest::gtest_main)
enable_testing()
include(GoogleTest)
gtest_discover_tests(example_tests)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
テストの実行
ctest --test-dir build --output-on-failure
ctest --test-dir build -R ClampTest
ctest --test-dir build -R "UserStoreTest.*" --output-on-failure
./build/example_tests --gtest_filter=ClampTest.*
./build/example_tests --gtest_filter=UserStoreTest.FindsExistingUser
失敗のデバッグ
- gtest フィルターを使って失敗した単一テストを再実行する。
- 失敗したアサーションの周りにスコープ付きログを追加する。
- サニタイザーを有効にして再実行する。
- 根本原因が特定できたらフルスイートに拡張する。
カバレッジ
グローバルフラグではなく、ターゲットレベルの設定を推奨。
option(ENABLE_COVERAGE "Enable coverage flags" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
target_compile_options(example_tests PRIVATE --coverage)
target_link_options(example_tests PRIVATE --coverage)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(example_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping)
target_link_options(example_tests PRIVATE -fprofile-instr-generate)
endif()
endif()
GCC + gcov + lcov:
cmake -S . -B build-cov -DENABLE_COVERAGE=ON
cmake --build build-cov -j
ctest --test-dir build-cov
lcov --capture --directory build-cov --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage
Clang + llvm-cov:
cmake -S . -B build-llvm -DENABLE_COVERAGE=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build-llvm -j
LLVM_PROFILE_FILE="build-llvm/default.profraw" ctest --test-dir build-llvm
llvm-profdata merge -sparse build-llvm/default.profraw -o build-llvm/default.profdata
llvm-cov report build-llvm/example_tests -instr-profile=build-llvm/default.profdata
サニタイザー
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=undefined)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
フレーキーテストのガードレール
- 同期に
sleepを使用しないでください。条件変数またはラッチを使用してください。 - 一時ディレクトリはテストごとに一意にし、常にクリーンアップしてください。
- 単体テストでは実時間、ネットワーク、ファイルシステムの依存を避けてください。
- ランダム入力には決定論的なシードを使用してください。
ベストプラクティス
推奨事項
- テストは決定論的かつ分離された状態に保つ
- グローバル変数よりも依存性注入を優先する
- 前提条件には
ASSERT_*、複数チェックにはEXPECT_*を使用する - 単体テストと結合テストを CTest ラベルまたはディレクトリで分離する
- CI でサニタイザーを実行してメモリと競合を検出する
非推奨事項
- 単体テストで実時間やネットワークに依存しない
- 条件変数が使える場合に同期に sleep を使用しない
- 単純な値オブジェクトを過剰にモックしない
- 重要でないログのために脆弱な文字列マッチングを使用しない
よくある落とし穴
- 固定の一時パスを使用する → テストごとに一意の一時ディレクトリを生成し、クリーンアップする。
- ウォールクロック時間に依存する → クロックを注入するか、偽の時間ソースを使用する。
- フレーキーな並行性テスト → 条件変数/ラッチと制限付き待機を使用する。
- 隠れたグローバル状態 → フィクスチャでグローバル状態をリセットするか、グローバルを削除する。
- 過剰なモック → ステートフルな振る舞いにはフェイクを優先し、インタラクションのみをモックする。
- サニタイザー実行の欠落 → CI に ASan/UBSan/TSan ビルドを追加する。
- デバッグ専用ビルドでのカバレッジ → カバレッジターゲットが一貫したフラグを使用することを確認する。
オプション付録: ファジング / プロパティベーステスト
プロジェクトがすでに LLVM/libFuzzer またはプロパティテスティングライブラリをサポートしている場合にのみ使用してください。
- libFuzzer: 最小限の I/O で純粋関数に最適。
- RapidCheck: 不変条件を検証するプロパティベーステスト。
最小限の libFuzzer ハーネス(疑似コード: ParseConfig を置き換え):
#include <cstddef>
#include <cstdint>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
std::string input(reinterpret_cast<const char *>(data), size);
// ParseConfig(input); // project function
return 0;
}
GoogleTest の代替
- Catch2: ヘッダオンリー、表現力豊かなマッチャー
- doctest: 軽量、最小限のコンパイルオーバーヘッド