claude-code/claude-zh/skills/cpp-testing/SKILL.md

323 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: cpp-testing
description: 僅在編寫/更新/修復 C++ 測試、配置 GoogleTest/CTest、診斷失敗或不穩定 (Flaky) 的測試,或增加覆蓋率/消毒器 (Sanitizers) 時使用。
---
# C++ 測試 (Agent 技能)
針對現代 C++ (C++17/20) 的 Agent 導向測試工作流,使用 GoogleTest/GoogleMock 並搭配 CMake/CTest。
## 何時使用
- 撰寫新 C++ 測試或修復現有測試。
- 為 C++ 組件設計單元/整合測試覆蓋。
- 增加測試覆蓋率、設置 CI 門檻或回歸保護。
- 為了一致的執行結果而配置 CMake/CTest 工作流。
- 調查測試失敗或不穩定 (Flaky) 的行為。
- 啟用消毒器 (Sanitizers) 進行記憶體或競爭診斷。
### 何時「不」使用
- 在無測試變更的情況下實作新產品功能。
- 與測試覆蓋或測試失敗無關的大規模重構。
- 在沒有測試回歸驗證的情況下進行效能調優。
- 非 C++ 專案或非測試性質的任務。
## 核心概念
- **TDD 迴圈**:紅燈 (Red) → 綠燈 (Green) → 重構 (Refactor) (先寫測試,執行最小化修復,最後進行清理)。
- **隔離性 (Isolation)**:優先選用相依注入 (Dependency Injection) 與 Fake 物件,而非使用全域狀態。
- **測試佈局**`tests/unit`, `tests/integration`, `tests/testdata`
- **Mock vs Fake**Mock 用於互動驗證Fake 用於模擬具備狀態的行為。
- **CTest 探索**:使用 `gtest_discover_tests()` 進行穩定的測試探索。
- **CI 信號**:先執行子集測試,再執行完整套件並附帶 `--output-on-failure` 選項。
## TDD 工作流
請遵循 紅燈 → 綠燈 → 重構 迴圈:
1. **紅燈 (RED)**:撰寫一個會失敗的測試,用以捕捉新的行為。
2. **綠燈 (GREEN)**:執行最小程度的變更以通過測試。
3. **重構 (REFACTOR)**:在保持綠燈的狀態下清理程式碼。
```cpp
// tests/add_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // 由生產環境程式碼提供。
TEST(AddTest, AddsTwoNumbers) { // 紅燈 (RED)
EXPECT_EQ(Add(2, 3), 5);
}
// src/add.cpp
int Add(int a, int b) { // 綠燈 (GREEN)
return a + b;
}
// 重構 (REFACTOR):測試通過後進行簡化或重新命名
```
## 程式碼範例
### 基本單元測試 (gtest)
```cpp
// tests/calculator_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // 由生產環境程式碼提供。
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
```
### 測試夾具 Fixture (gtest)
```cpp
// tests/user_store_test.cpp
// 偽程式碼存根:請用專案實際型別替代 UserStore/User。
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
struct User { std::string name; };
class UserStore {
public:
explicit UserStore(std::string /*路徑*/) {}
void Seed(std::initializer_list<User> /*使用者清單*/) {}
std::optional<User> Find(const std::string &/*名稱*/) { 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");
}
```
### Mock 模擬 (gmock)
```cpp
// 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 &notifier) : notifier_(notifier) {}
void Publish(const std::string &message) { notifier_.Send(message); }
private:
Notifier &notifier_;
};
TEST(ServiceTest, SendsNotifications) {
MockNotifier notifier;
Service service(notifier);
EXPECT_CALL(notifier, Send("hello")).Times(1);
service.Publish("hello");
}
```
### CMake/CTest 快速上手
```cmake
# CMakeLists.txt (節錄)
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# 優先使用專案鎖定的版本。若使用標籤,請根據專案政策使用固定的版本。
set(GTEST_VERSION v1.17.0) # 根據專案政策調整。
FetchContent_Declare(
googletest
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)
```
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
```
## 執行測試
```bash
ctest --test-dir build --output-on-failure
ctest --test-dir build -R ClampTest
ctest --test-dir build -R "UserStoreTest.*" --output-on-failure
```
```bash
./build/example_tests --gtest_filter=ClampTest.*
./build/example_tests --gtest_filter=UserStoreTest.FindsExistingUser
```
## 偵錯失敗
1. 使用 gtest 篩選器重新執行單一失敗的測試。
2. 在失敗的斷言周圍加入作用域日誌 (Scoped logging)。
3. 在啟用消毒器 (Sanitizers) 的情況下重新執行。
4. 當根因修復後,擴展至執行完整套件。
## 覆蓋率 (Coverage)
優先選用目標層級 (Target-level) 的設定,而非使用全域旗標。
```cmake
option(ENABLE_COVERAGE "啟用覆蓋率旗標" 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:
```bash
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:
```bash
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
```
## 消毒器 (Sanitizers)
```cmake
option(ENABLE_ASAN "啟用 AddressSanitizer" OFF)
option(ENABLE_UBSAN "啟用 UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "啟用 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 Tests) 防範機制
- 絕不使用 `sleep` 進行同步;應使用條件變數 (Condition variables) 或 Latches。
- 確保每個測試的暫存目錄是唯一的,並始終清理乾淨。
- 單元測試應避免依賴真實時間、網路或檔案系統。
- 對隨機化輸入使用確定性的種子。
## 最佳實踐
### 推薦做法 (DO)
- 保持測試的確定性與隔離性。
- 優先選用相依注入 (Dependency injection) 而非全域變數。
- 使用 `ASSERT_*` 檢查前提條件,使用 `EXPECT_*` 進行多重檢查。
- 在 CTest 標籤或目錄中區分單元測試與整合測試。
- 在 CI 中執行消毒器,以偵測記憶體問題與競爭情況。
### 應避免的做法 (DON'T)
- 單元測試不應依賴真實時間或網路。
- 在可使用條件變數的情況下,不要使用 sleep 進行同步。
- 不要對簡單的數值物件 (Value objects) 進行過度 Mock 模擬。
- 對於非關鍵字日誌,不要使用脆弱的字串匹配。
### 常見陷阱
- **使用固定的暫存路徑** → 每個測試生成唯一的暫存目錄並進行清理。
- **依賴掛鐘時間 (Wall clock time)** → 注入時鐘或使用 Fake 時間源。
- **不穩定的併發測試** → 使用條件變數/Latches 以及帶超時的等待。
- **隱藏的全域狀態** → 在 Fixtures 中重置全域狀態或移除全域變數。
- **過度 Mock 模擬 (Over-mocking)** → 針對具備狀態的行為優先使用 Fake 物件,僅針對互動進行 Mock 模擬。
- **遺漏消毒器執行** → 在 CI 中加入 ASan/UBSan/TSan 建置。
- **僅在 Debug 建置中檢查覆蓋率** → 確保覆蓋率目標使用一致的旗標。
## 補充附錄:模糊測試 (Fuzzing) / 屬性測試 (Property Testing)
僅在專案已支援 LLVM/libFuzzer 或屬性測試函式庫時使用。
- **libFuzzer**:最適合幾乎不涉及 I/O 的純函式。
- **RapidCheck**:基於屬性的測試,用以驗證不變式。
最簡 libFuzzer 載體 (偽程式碼:請替換 ParseConfig)
```cpp
#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); // 專案函式
return 0;
}
```
## GoogleTest 的替代方案
- **Catch2**Header-only表達力豐富的匹配器 (Matchers)。
- **doctest**:輕量級,編譯開銷極小。