| name | cpp-testing |
| description | C++ 테스트 작성/업데이트/수정, GoogleTest/CTest 구성, 실패하거나 불안정한 테스트 진단, 또는 커버리지/새니타이저(sanitizer) 추가 시에만 사용하십시오. |
| origin | ECC |
C++ 테스트 (Agent Skill)
CMake/CTest와 함께 GoogleTest/GoogleMock을 사용하는 모던 C++(C++17/20)용 에이전트 중심 테스트 워크플로입니다.
사용 시점
- 새로운 C++ 테스트를 작성하거나 기존 테스트를 수정할 때
- C++ 컴포넌트를 위한 단위/통합 테스트 커버리지를 설계할 때
- 테스트 커버리지, CI 게이트 또는 회귀 방지를 추가할 때
- 일관된 실행을 위해 CMake/CTest 워크플로를 구성할 때
- 테스트 실패 또는 불안정한(flaky) 동작을 조사할 때
- 메모리/레이스(race) 진단을 위해 새니타이저를 활성화할 때
사용하지 않는 경우
- 테스트 변경 없이 새로운 제품 기능을 구현할 때
- 테스트 커버리지나 실패와 무관한 대규모 리팩터링을 할 때
- 검증할 테스트 회귀 없이 성능 튜닝을 할 때
- 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 워크플로
RED → GREEN → REFACTOR 루프를 따르십시오.
- RED: 새로운 동작을 캡처하는 실패하는 테스트 작성
- GREEN: 통과를 위한 최소한의 변경 구현
- REFACTOR: 테스트가 통과하는 상태를 유지하면서 코드 정리
#include <gtest/gtest.h>
int Add(int a, int b);
TEST(AddTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
int Add(int a, int b) {
return a + b;
}
코드 예시
기본 단위 테스트 (gtest)
#include <gtest/gtest.h>
int Add(int a, int b);
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
픽스처 (Fixture - gtest)
#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();
(user.());
(user->name, );
}
Mock (gmock)
#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.();
}
CMake/CTest 퀵스타트
# 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
# Google Test 프레임워크 (공식 저장소)
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 필터를 사용하여 실패한 개별 테스트를 재실행하십시오.
- 실패한 어설션(assertion) 주변에 스코프 로그를 추가하십시오.
- 새니타이저를 활성화하여 재실행하십시오.
- 근본 원인이 해결되면 전체 스위트로 확장하십시오.
커버리지 (Coverage)
글로벌 플래그 대신 타겟 레벨 설정을 권장합니다.
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
새니타이저 (Sanitizers)
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 Test) 방지 가드레일
- 동기화를 위해 절대
sleep을 사용하지 마십시오. 조건 변수(condition variable)나 래치(latch)를 사용하십시오.
- 임시 디렉터리는 테스트마다 고유하게 생성하고 항상 정리하십시오.
- 단위 테스트에서는 실제 시간, 네트워크 또는 파일 시스템 종속성을 피하십시오.
- 무작위 입력에는 결정론적 시드(seed)를 사용하십시오.
모범 사례
권장 사항 (DO)
- 테스트를 결정론적이고 격리된 상태로 유지하십시오.
- 글로벌 변수보다 의존성 주입을 선호하십시오.
- 전제 조건에는
ASSERT_*를, 여러 개의 체크에는 EXPECT_*를 사용하십시오.
- CTest 레이블이나 디렉터리를 사용하여 단위 테스트와 통합 테스트를 분리하십시오.
- 메모리 및 레이스 감지를 위해 CI에서 새니타이저를 실행하십시오.
금지 사항 (DON'T)
- 단위 테스트에서 실제 시간이나 네트워크에 의존하지 마십시오.
- 조건 변수를 사용할 수 있는 곳에 동기화 수단으로 sleep을 쓰지 마십시오.
- 단순한 값 객체(value object)를 과도하게 모킹하지 마십시오.
- 중요하지 않은 로그에 대해 깨지기 쉬운 문자열 매칭을 사용하지 마십시오.
흔한 함정
- 고정된 임시 경로 사용 → 테스트마다 고유한 임시 디렉터리를 생성하고 정리하십시오.
- 실제 시계 시간(wall clock time)에 의존 → 클록을 주입하거나 가짜 시간 소스를 사용하십시오.
- 불안정한 동시성 테스트 → 조건 변수/래치와 유한 대기(bounded wait)를 사용하십시오.
- 숨겨진 글로벌 상태 → 픽스처에서 글로벌 상태를 재설정하거나 글로벌을 제거하십시오.
- 과도한 모킹(Over-mocking) → 상태 저장 동작에는 페이크를 선호하고, 상호작용만 모킹하십시오.
- 새니타이저 실행 누락 → CI에 ASan/UBSan/TSan 빌드를 추가하십시오.
- 디버그 전용 빌드에서만 커버리지 확인 → 커버리지 타겟이 일관된 플래그를 사용하는지 확인하십시오.
선택적 부록: 퍼징(Fuzzing) / 속성 기반 테스트(Property Testing)
프로젝트가 이미 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);
return 0;
}
GoogleTest의 대안
- Catch2: 헤더 전용, 표현력 있는 매처(matcher) 제공
- doctest: 가볍고 컴파일 오버헤드가 최소화됨