C++ テスト (エージェントスキル)
GoogleTest/GoogleMockとCMake/CTestを使用した、最新C++(C++17/20)向けのエージェント指向テストワークフロー。
使用すべきケース
- 新しいC++テストの作成、または既存テストの修正
- C++コンポーネントに対する単体テスト・統合テストの設計
- テストカバレッジ、CIゲート、回帰テストの追加
- 一貫した実行のためのCMake/CTestワークフローの設定
- テスト失敗やflaky動作の調査
- メモリ/競合診断のためのサニタイザの有効化
使用すべきでないケース
- テスト変更を伴わない新機能の実装
- テストカバレッジや失敗に関係のない大規模リファクタリング
- テスト回帰検証がないパフォーマンスチューニング
- 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()
Flakyテストのガードレール
- 同期に
sleepを決して使用しない。条件変数またはラッチを使用すること。 - テストごとに一時ディレクトリを一意にし、常にクリーンアップすること。
- 単体テストでは、実時間、ネットワーク、ファイルシステムへの依存を避けること。
- ランダム入力には決定論的なシードを使用すること。
ベストプラクティス
すべきこと
- テストを決定論的かつ分離して維持する
- グローバルよりも依存性注入を優先する
- 前提条件には
ASSERT_*を、複数チェックにはEXPECT_*を使用する - 単体テストと統合テストをCTestラベルまたはディレクトリで分離する
- CIでサニタイザを実行してメモリと競合を検出する
すべきでないこと
- 単体テストで実時間やネットワークに依存しない
- 条件変数が使える場合にスリープを同期に使用しない
- 単純な値オブジェクトを過剰にモックしない
- 重要でないログに対して脆弱な文字列マッチングを使用しない
よくある落とし穴
- 固定の一時パスを使用する → テストごとに一意の一時ディレクトリを生成し、クリーンアップする。
- 実時間に依存する → クロックを注入するか、疑似時刻ソースを使用する。
- Flakyな並行テスト → 条件変数/ラッチと制限付き待機を使用する。
- 隠れたグローバル状態 → フィクスチャでグローバル状態をリセットするか、グローバルを削除する。
- 過剰なモック → 状態動作にはフェイクを優先し、相互作用のみをモックする。
- サニタイザ実行の欠落 → 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: 軽量、最小限のコンパイルオーバーヘッド