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); // 프로덕션 코드에서 제공.
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)
// tests/calculator_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // 프로덕션 코드에서 제공.
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
픽스처 (gtest)
// 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 /*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 (발췌)
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 필터로 실패한 단일 테스트를 다시 실행합니다.
- 실패한 어설션 주변에 범위가 지정된 로깅을 추가합니다.
- 샌타타이저를 활성화하여 다시 실행합니다.
- 근본 원인이 해결되면 전체 스위트로 확장합니다.
커버리지
전역 플래그보다는 타겟 수준 설정을 선호합니다.
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을 절대 사용하지 말고, 조건 변수 또는 래치를 사용하세요. - 임시 디렉토리는 테스트마다 고유하게 생성하고 항상 정리하세요.
- 단위 테스트에서 실제 시간, 네트워크 또는 파일 시스템 의존성을 피하세요.
- 무작위 입력에는 결정론적 시드를 사용하세요.
모범 사례
따라야 할 것 (DO)
- 테스트를 결정론적이고 격리되게 유지하세요.
- 전역 변수보다 의존성 주입을 선호하세요.
- 전제 조건에는
ASSERT_*, 여러 검사에는EXPECT_*를 사용하세요. - CTest 레이블이나 디렉토리로 단위 테스트와 통합 테스트를 분리하세요.
- CI에서 메모리 및 경쟁 상태 탐지를 위해 샌타타이저를 실행하세요.
하지 말아야 할 것 (DON'T)
- 단위 테스트에서 실제 시간이나 네트워크에 의존하지 마세요.
- 조건 변수를 사용할 수 있는데 동기화에 sleep을 사용하지 마세요.
- 단순 값 객체를 과도하게 목(mock)하지 마세요.
- 중요하지 않은 로그에 대해 취약한 문자열 매칭을 사용하지 마세요.
일반적인 함정
- 고정된 임시 경로 사용 → 테스트마다 고유한 임시 디렉토리를 생성하고 정리하세요.
- 실제 시간에 의존 → 클록을 주입하거나 가짜 시간 소스를 사용하세요.
- 플래키 동시성 테스트 → 조건 변수/래치와 제한된 대기를 사용하세요.
- 숨겨진 전역 상태 → 픽스처에서 전역 상태를 재설정하거나 제거하세요.
- 과도한 목(mock) 사용 → 상태 유지 동작에는 페이크를 선호하고 상호작용만 목(mock)하세요.
- 샌타타이저 실행 누락 → 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); // 프로젝트 함수
return 0;
}
GoogleTest 대안
- Catch2: 헤더 온리, 표현력 풍부한 매처
- doctest: 경량, 최소 컴파일 오버헤드