C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
이 글의 핵심
Google Test(gtest) 단위 테스트 가이드. FetchContent·vcpkg 설치, TEST·TEST_F·EXPECT·ASSERT·파라미터화·Death Test, TDD·프로덕션 패턴, CI 연동, 자주 나는 오류 해결.
들어가며: “코드를 수정했는데 다른 곳이 깨졌어요”
리팩토링 후 예상치 못한 버그
함수를 리팩토링(코드 구조를 바꾸되 동작은 유지하는 것)했습니다. 테스트 코드가 없어서 어디가 깨졌는지 몰랐습니다.
단위 테스트(unit test—함수·클래스 단위로 입력과 기대 출력을 검증하는 테스트)는 “이 함수/클래스가 주어진 입력에 대해 기대한 결과를 내는지”를 자동으로 검사해서, 수정 후 회귀 버그(이전에 되던 기능이 깨지는 버그)를 바로 잡을 수 있게 해 줍니다. Google Test처럼 ASSERT/EXPECT와 픽스처만 익혀도 핵심 로직에 대한 테스트를 빠르게 추가할 수 있고, CI에 넣어 두면 커밋 시마다 안정성을 확인할 수 있습니다.
단위 테스트는 모든 언어에서 중요합니다. Python에서 pytest·CI, Node.js의 Jest, Go의 go test, Rust의 cargo test는 각각의 생태계에서 표준에 가깝습니다. 커버리지·CI와 이어서 읽으면 C++ 코드 커버리지, C++ GitHub Actions 멀티 OS 빌드, Node.js GitHub Actions CI/CD가 같은 그림을 완성합니다.
문제 상황: calculate를 a + b에서 a * b로 잘못 바꾸면, 테스트가 없을 때는 실제 사용 중에만 버그가 드러납니다. 단위 테스트가 있으면 EXPECT_EQ(calculate(2, 3), 5) 한 줄로 “2+3은 5여야 한다”를 검증하므로, 리팩토링 후 바로 실패해 원인을 찾기 쉽습니다.
// 리팩토링 전
int calculate(int a, int b) {
return a + b;
}
// 리팩토링 후 (버그 발생)
int calculate(int a, int b) {
return a * b; // ❌ 실수로 + 대신 *
}
// 테스트 없음 → 버그 발견 못함
// 배포 후 고객 환경에서만 "결과가 이상해요" 신고로 발견됨
요구 환경: Google Test(GTest)가 필요합니다. vcpkg(vcpkg install gtest), Conan, 또는 FetchContent로 프로젝트에 포함해 빌드. g++/Clang + C++14 이상, Windows에서는 MSVC + vcpkg 권장.
Google Test로 해결: TEST(테스트스위트이름, 테스트이름) 매크로로 하나의 테스트 케이스를 정의합니다. **EXPECT_EQ(실제값, 기대값)은 같지 않으면 실패 메시지를 출력하지만 테스트는 계속 진행하고, ASSERT_EQ**를 쓰면 실패 시 그 시점에서 중단됩니다. 위처럼 기대값 5를 넣어 두면, calculate가 6을 반환할 때 “Expected: 5, Actual: 6”으로 버그를 즉시 발견할 수 있습니다.
TEST(CalculateTest, Addition) {
EXPECT_EQ(calculate(2, 3), 5); // ✅ 테스트 실패로 버그 발견
}
// 출력:
// Expected: 5
// Actual: 6
// 버그 즉시 발견!
이 글을 읽으면:
- Google Test로 단위 테스트를 작성할 수 있습니다.
- 테스트 픽스처로 테스트를 구조화할 수 있습니다.
- Mock 객체로 의존성을 분리할 수 있습니다.
- 실전에서 테스트 가능한 코드를 작성할 수 있습니다.
실무에서 자주 겪는 문제 시나리오
시나리오 1: “배포 후 고객사에서만 재현되는 버그”
상황: parseUserInput() 함수가 빈 문자열 ""을 입력받으면 size()가 0인데, vec[0]에 접근해 크래시가 발생했습니다. 개발자 PC에서는 빈 입력을 테스트하지 않아서, 배포 후 고객사에서만 재현되었습니다.
해결: 경계값 테스트를 추가해 parseUserInput("")가 예외를 던지거나 안전한 값을 반환하는지 검증합니다.
TEST(ParseUserInputTest, EmptyStringThrowsOrReturnsSafe) {
EXPECT_THROW(parseUserInput(""), std::invalid_argument);
// 또는 예외 대신 안전한 기본값 반환 시:
// EXPECT_EQ(parseUserInput("").size(), 0);
}
시나리오 2: “다른 팀원이 내 코드를 수정했는데 동작이 바뀌었어요”
상황: validateOrder() 함수가 “주문 금액이 0 이하면 거부”해야 하는데, 누군가 if (amount >= 0)로 잘못 수정해 0원 주문이 통과했습니다. 테스트가 없어서 코드 리뷰에서도 놓쳤습니다.
해결: 경계값 테스트로 validateOrder(0)이 실패하는지 검증합니다.
TEST(ValidateOrderTest, ZeroAmountRejected) {
EXPECT_FALSE(validateOrder(0));
EXPECT_FALSE(validateOrder(-100));
}
시나리오 3: “DB 연결·파일 I/O 때문에 테스트가 느리고 불안정해요”
상황: UserService가 실제 DB에 연결해 테스트를 돌리면, CI에서 느리고 네트워크가 끊기면 실패합니다.
해결: Mock 객체(Google Mock)로 DB 의존성을 제거하고, “이 입력이 들어오면 이 결과를 반환한다”만 검증합니다. 이 글은 gtest 기초를 다루고, Google Mock(#18-2)에서 Mock 사용법을 자세히 다룹니다.
시나리오 4: “테스트가 너무 많아서 CI가 10분 넘게 걸려요”
상황: 500개 이상의 테스트가 있고, 각각 대용량 파일을 읽거나 네트워크를 호출해 CI가 느립니다.
해결: 단위 테스트는 빠르고 실행되어야 합니다. 외부 의존성은 Mock으로 대체하고, 통합 테스트는 별도 스위트로 분리해 --gtest_filter로 선택 실행합니다.
시나리오 5: “assert 실패 시 크래시만 나고 원인을 못 찾아요”
상황: assert(ptr != nullptr)로 널 포인터를 검사하는데, 프로덕션에서 크래시가 발생합니다. assert가 제대로 동작하는지, 잘못된 입력 시 즉시 종료하는지 검증하고 싶습니다.
해결: Death Test(죽음 테스트)로 “이 코드가 실행되면 프로세스가 종료되어야 한다”를 검증합니다. EXPECT_DEATH로 널 포인터 접근·잘못된 인자 시 abort() 또는 exit()가 호출되는지 확인할 수 있습니다.
시나리오 6: “테스트 간 순서에 따라 결과가 달라져요”
상황: 전역 변수나 싱글톤을 사용하는 테스트가 있어, 실행 순서에 따라 통과/실패가 바뀝니다.
해결: 테스트는 독립적이어야 합니다. 전역 상태 대신 픽스처의 SetUp()/TearDown()에서 초기화하고, 각 테스트가 서로 영향을 주지 않도록 설계합니다.
테스트 실행 흐름
flowchart TD
subgraph init["초기화"]
A[main 시작] --> B[InitGoogleTest]
B --> C[테스트 스위트 등록]
end
subgraph run["테스트 실행"]
C --> D[SetUpTestSuite]
D --> E[각 테스트마다]
E --> F[SetUp]
F --> G[TEST 실행]
G --> H[TearDown]
H --> E
end
subgraph cleanup["정리"]
E --> I[TearDownTestSuite]
I --> J[결과 출력]
end
목차
- Google Test 시작하기
- 기본 테스트 작성
- 테스트 픽스처
- 파라미터화 테스트
- Death Test (죽음 테스트)
- 테스트 패턴 (AAA, Given-When-Then)
- TDD 실전
- 실전 패턴
- 프로덕션 패턴
- 모범 사례 (Best Practices)
- 자주 발생하는 오류와 해결
1. Google Test 시작하기
설치 (CMake + FetchContent)
FetchContent로 Google Test 저장소를 지정하고 GIT_TAG로 버전을 고정합니다. FetchContent_MakeAvailable(googletest)가 configure 시점에 소스를 받아 빌드에 포함시키므로, 프로젝트에 서브모듈을 넣지 않아도 됩니다. gtest_main을 링크하면 main이 자동으로 제공되어, TEST만 정의해도 실행 파일이 됩니다. add_test로 CTest에 등록하면 ctest로 테스트를 실행할 수 있습니다.
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(myapp_test test_main.cpp)
target_link_libraries(myapp_test PRIVATE
gtest_main
)
add_test(NAME myapp_test COMMAND myapp_test)
vcpkg 설치 (대안)
vcpkg install gtest
find_package(GTest REQUIRED)
target_link_libraries(myapp_test PRIVATE GTest::gtest_main)
첫 테스트 (완전한 실행 가능 예제)
**#include <gtest/gtest.h>**만 하면 TEST, EXPECT_EQ 등을 쓸 수 있습니다. TEST(AddTest, PositiveNumbers)는 “AddTest” 스위트의 “PositiveNumbers” 테스트 하나를 만듭니다. EXPECT_EQ(add(2, 3), 5)는 add(2,3)이 5와 같지 않으면 실패합니다. 테스트 이름은 “동작을 설명하는 문장”처럼 짓는 것이 좋습니다(예: Addition_SumsTwoPositiveNumbers).
// test_main.cpp - 복사해 붙여넣은 뒤 CMake로 빌드하거나
// g++ -std=c++17 test_main.cpp -lgtest -lgtest_main -lpthread -o test && ./test
#include <gtest/gtest.h>
int add(int a, int b) {
return a + b;
}
TEST(AddTest, PositiveNumbers) {
EXPECT_EQ(add(2, 3), 5);
}
TEST(AddTest, NegativeNumbers) {
EXPECT_EQ(add(-2, -3), -5);
}
TEST(AddTest, ZeroAndPositive) {
EXPECT_EQ(add(0, 5), 5);
EXPECT_EQ(add(5, 0), 5);
}
실행 결과:
$ ./myapp_test
[==========] Running 3 tests from 1 test suite.
[----------] 3 tests from AddTest
[ RUN ] AddTest.PositiveNumbers
[ OK ] AddTest.PositiveNumbers (0 ms)
[ RUN ] AddTest.NegativeNumbers
[ OK ] AddTest.NegativeNumbers (0 ms)
[ RUN ] AddTest.ZeroAndPositive
[ OK ] AddTest.ZeroAndPositive (0 ms)
[----------] 3 tests from AddTest (0 ms total)
[==========] 3 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 3 tests.
특정 테스트만 실행하기
# 특정 테스트만 실행
./myapp_test --gtest_filter=AddTest.PositiveNumbers
# 패턴으로 여러 테스트 실행
./myapp_test --gtest_filter=AddTest.*
# 반복 실행 (회귀 버그 디버깅 시 유용)
./myapp_test --gtest_repeat=10
2. 기본 테스트 작성
Assertion 종류
EXPECT_* 시리즈는 실패해도 나머지 테스트가 계속 실행되어, 한 테스트 안에서 여러 개를 검사할 때 유리합니다. ASSERT_* 시리즈는 실패 시 그 지점에서 중단되므로, “이 조건이 아니면 아래 검증이 의미 없을 때”(예: 포인터가 null이 아닌지 확인한 뒤 역참조) 사용합니다. EXPECT_LT/EXPECT_GT 등은 크기 비교, EXPECT_TRUE/EXPECT_FALSE는 불린 값 검증에 씁니다.
// EXPECT: 실패해도 계속 실행
EXPECT_EQ(a, b); // a == b
EXPECT_NE(a, b); // a != b
EXPECT_LT(a, b); // a < b
EXPECT_LE(a, b); // a <= b
EXPECT_GT(a, b); // a > b
EXPECT_GE(a, b); // a >= b
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);
// ASSERT: 실패하면 즉시 종료
ASSERT_EQ(a, b);
ASSERT_TRUE(condition);
문자열 비교
std::string은 EXPECT_EQ로 비교할 수 있습니다. C 문자열(const char*)끼리는 EXPECT_STREQ, EXPECT_STRNE를 쓰면 null 종료까지 비교하고, 포인터 주소가 아닌 내용으로 비교합니다. strcmp 기반이라 대소문자 구분됩니다.
TEST(StringTest, Comparison) {
std::string str = "Hello";
EXPECT_EQ(str, "Hello");
EXPECT_NE(str, "World");
// C 문자열
EXPECT_STREQ("hello", "hello");
EXPECT_STRNE("hello", "world");
}
부동소수점 비교
부동소수점은 비트 단위로 완전히 같지 않을 수 있어 EXPECT_EQ(a, b)는 실패할 수 있습니다(예: 0.1+0.2 != 0.3). EXPECT_DOUBLE_EQ는 내부적으로 작은 오차를 허용하고, EXPECT_NEAR(a, b, tol)는 tol 이내로 차이가 나면 통과합니다. 성능·수치 테스트에서 “대략 이 범위”를 검증할 때 EXPECT_NEAR를 쓰면 좋습니다.
TEST(FloatTest, Comparison) {
double a = 0.1 + 0.2;
double b = 0.3;
// ❌ 정확한 비교 (실패할 수 있음)
// EXPECT_EQ(a, b);
// ✅ 근사 비교
EXPECT_DOUBLE_EQ(a, b);
EXPECT_NEAR(a, b, 0.0001);
}
예외 테스트
EXPECT_THROW(표현식, 예외타입)은 해당 표현식이 정확히 그 예외 타입을 던지면 성공합니다. 타입이 다르거나 예외가 안 나오면 실패합니다. EXPECT_NO_THROW는 예외가 나오면 실패합니다. 나눗셈·파일 열기·유효성 검사처럼 “잘못된 입력 시 이 예외를 던져야 한다”를 명시적으로 테스트할 때 유용합니다.
int divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}
TEST(DivideTest, ThrowsOnZero) {
EXPECT_THROW(divide(10, 0), std::invalid_argument);
EXPECT_NO_THROW(divide(10, 2));
}
예외 메시지 검증
예외가 던져질 때 메시지 내용까지 검증하려면 EXPECT_THROW 내부에서 try-catch를 사용합니다.
TEST(DivideTest, ThrowsOnZeroWithCorrectMessage) {
try {
divide(10, 0);
FAIL() << "Expected std::invalid_argument";
} catch (const std::invalid_argument& e) {
EXPECT_STREQ(e.what(), "Division by zero");
}
}
3. 테스트 픽스처
기본 픽스처
::testing::Test를 상속한 픽스처 클래스를 만들고, SetUp()에서 각 테스트 실행 전에 공통 초기화를 합니다. TearDown()은 각 테스트 실행 후 정리용입니다. TEST_F(StackTest, Size)처럼 TEST_F(픽스처클래스, 테스트이름)를 쓰면 픽스처가 생성되고 SetUp이 호출된 뒤 테스트가 돌아갑니다. 이렇게 하면 “스택에 1, 2가 들어 있는 상태”를 여러 테스트에서 반복하지 않고 재사용할 수 있습니다.
class StackTest : public ::testing::Test {
protected:
void SetUp() override {
// 각 테스트 전에 실행
stack.push(1);
stack.push(2);
}
void TearDown() override {
// 각 테스트 후에 실행
}
std::stack<int> stack;
};
TEST_F(StackTest, Size) {
EXPECT_EQ(stack.size(), 2);
}
TEST_F(StackTest, Pop) {
stack.pop();
EXPECT_EQ(stack.size(), 1);
}
공유 리소스 (SetUpTestSuite / TearDownTestSuite)
SetUpTestSuite / TearDownTestSuite는 해당 테스트 스위트 전체에서 한 번만 실행됩니다. DB 연결·서버 기동처럼 비용이 큰 리소스는 여기서 한 번 만들고, static 멤버로 모든 테스트가 공유하게 할 수 있습니다. 테스트 간 상태가 격리되지 않으므로, 테스트가 서로 영향을 주지 않도록 각 테스트에서 데이터를 정리하거나 트랜잭션을 롤백하는 방식으로 설계하는 것이 좋습니다.
class DatabaseTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
// 모든 테스트 전에 한 번만 실행
db = new Database("test.db");
}
static void TearDownTestSuite() {
// 모든 테스트 후에 한 번만 실행
delete db;
}
static Database* db;
};
Database* DatabaseTest::db = nullptr;
TEST_F(DatabaseTest, Insert) {
db->insert("key", "value");
EXPECT_EQ(db->get("key"), "value");
}
4. 파라미터화 테스트
기본 파라미터화
TestWithParam<타입>으로 파라미터 타입을 정하고, TEST_P(이름, 테스트)로 테스트를 작성합니다. GetParam()으로 현재 파라미터를 받아서 사용합니다. INSTANTIATE_TEST_SUITE_P로 (a, b, expected) 조합을 Values나 make_tuple로 나열하면, 각 조합마다 테스트가 하나씩 생성됩니다. 같은 로직을 여러 입력으로 반복 검증할 때 중복을 줄일 수 있습니다.
class AddTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {
};
TEST_P(AddTest, Addition) {
auto [a, b, expected] = GetParam();
EXPECT_EQ(add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(
AddTestCases,
AddTest,
::testing::Values(
std::make_tuple(2, 3, 5),
std::make_tuple(-2, -3, -5),
std::make_tuple(0, 0, 0),
std::make_tuple(100, 200, 300)
)
);
범위 파라미터
::testing::Range(시작, 끝)은 시작 이상 끝 미만의 정수 시퀀스를 만들어, 그 값들이 각각 GetParam()으로 넘어갑니다. 위 예는 0~99에 대해 square(n) >= 0인지 검사합니다. Range(0, 100, 10)처럼 세 번째 인자로 step을 줄 수도 있어, 경계값·몇 개 샘플만 골라서 테스트할 때 유용합니다.
int square(int n) { return n * n; }
class SquareTest : public ::testing::TestWithParam<int> {
};
TEST_P(SquareTest, NonNegative) {
int n = GetParam();
EXPECT_GE(square(n), 0);
}
INSTANTIATE_TEST_SUITE_P(
SquareTestCases,
SquareTest,
::testing::Range(0, 100) // 0부터 99까지
);
Combine로 여러 파라미터 조합
class ParamTest : public ::testing::TestWithParam<std::tuple<int, bool>> {};
TEST_P(ParamTest, Combination) {
auto [value, flag] = GetParam();
// value와 flag 조합별로 테스트
}
INSTANTIATE_TEST_SUITE_P(
AllCombinations,
ParamTest,
::testing::Combine(
::testing::Values(1, 2, 3),
::testing::Bool()
)
);
5. Death Test (죽음 테스트)
Death Test는 “이 코드가 실행되면 프로세스가 종료되어야 한다”를 검증합니다. assert 실패, abort(), exit(n) 호출, 널 포인터 역참조 등 치명적 오류가 발생했을 때 프로그램이 제대로 종료하는지 확인할 때 사용합니다. Google Test는 death test를 별도 자식 프로세스에서 실행해, 부모 프로세스가 크래시하지 않도록 합니다.
EXPECT_DEATH / ASSERT_DEATH
EXPECT_DEATH(문장, 정규식)은 문장이 실행되면 프로세스가 0이 아닌 종료 코드로 종료되고, stderr 출력이 정규식과 일치하면 성공합니다. ASSERT_DEATH는 실패 시 해당 테스트에서 즉시 중단합니다.
#include <gtest/gtest.h>
#include <cstdlib>
#include <cassert>
void mustNotBeNull(const int* ptr) {
assert(ptr != nullptr && "ptr must not be null");
// ptr 사용...
}
int divideOrExit(int a, int b) {
if (b == 0) {
std::cerr << "Fatal: division by zero" << std::endl;
std::exit(1);
}
return a / b;
}
// Death Test: assert 실패 시 abort 검증
TEST(DeathTest, MustNotBeNull_AbortsOnNull) {
int* ptr = nullptr;
ASSERT_DEATH(mustNotBeNull(ptr), "ptr must not be null");
}
// Death Test: exit(1) 호출 검증
TEST(DeathTest, DivideOrExit_ExitsOnZero) {
EXPECT_DEATH(divideOrExit(10, 0), "Fatal: division by zero");
}
// 복합 문장 사용 (여러 줄)
TEST(DeathTest, CompoundStatement) {
int x = 0;
ASSERT_DEATH({
int* p = nullptr;
*p = 42; // 널 포인터 역참조 → SIGSEGV
}, "");
}
EXPECT_EXIT / ASSERT_EXIT (종료 코드·시그널 검증)
EXPECT_EXIT(문장, predicate, 정규식)은 종료 조건을 세밀하게 지정할 수 있습니다. ::testing::ExitedWithCode(n)은 exit(n)으로 종료된 경우를 검증합니다.
TEST(DeathTest, NormalExitWithCode) {
EXPECT_EXIT(
std::exit(42),
::testing::ExitedWithCode(42),
""
);
}
Death Test 네이밍 규칙
Death test가 포함된 테스트 스위트는 ***DeathTest**로 끝나도록 이름을 짓습니다. Google Test는 *DeathTest 스위트를 다른 테스트보다 먼저 실행해 fork 관련 문제를 줄입니다.
TEST(ValidationDeathTest, NullPointerCrashes) {
ASSERT_DEATH(process(nullptr), "");
}
EXPECT_DEATH_IF_SUPPORTED (호환성)
일부 플랫폼(예: Windows의 일부 설정)에서는 death test가 지원되지 않을 수 있습니다. EXPECT_DEATH_IF_SUPPORTED는 지원 시에만 실행하고, 미지원 시 스킵합니다.
TEST(DeathTest, OptionalOnUnsupportedPlatform) {
EXPECT_DEATH_IF_SUPPORTED(riskyOperation(), "error");
}
6. 테스트 패턴 (AAA, Given-When-Then)
AAA 패턴 (Arrange-Act-Assert)
테스트를 준비(Arrange) → 실행(Act) → 검증(Assert) 세 단계로 나누면 가독성이 좋아집니다.
TEST(CalculatorTest, SubtractPositiveNumbers) {
// Arrange: 테스트 데이터 준비
int a = 10;
int b = 3;
// Act: 실행할 동작
int result = subtract(a, b);
// Assert: 검증
EXPECT_EQ(result, 7);
}
Given-When-Then (BDD 스타일)
“주어진 조건에서, 어떤 동작을 하면, 이 결과가 나와야 한다”를 명시합니다.
TEST(AccountTest, WithdrawDecreasesBalance) {
// Given: 1000원 잔액의 계좌
Account account(1000);
// When: 300원 출금
account.withdraw(300);
// Then: 잔액은 700원
EXPECT_EQ(account.getBalance(), 700);
}
한 테스트 하나의 검증 (One Assert)
한 테스트는 하나의 동작만 검증하는 것이 좋습니다. 여러 검증이 필요하면 테스트를 나눕니다. 단, 같은 논리적 단위를 검증하는 여러 EXPECT는 한 테스트에 둘 수 있습니다.
// ✅ 좋은 예: 하나의 동작 검증
TEST(VectorTest, PushBackIncreasesSize) {
std::vector<int> vec;
vec.push_back(1);
EXPECT_EQ(vec.size(), 1);
}
// ⚠️ 한 테스트에 여러 검증이지만, 같은 논리적 단위면 허용
TEST(VectorTest, InitialState) {
std::vector<int> vec;
EXPECT_TRUE(vec.empty());
EXPECT_EQ(vec.size(), 0);
}
7. TDD 실전
TDD 사이클
flowchart LR
R[Red] --> G[Green]
G --> Ref[Refactor]
Ref --> R
- Red: 실패하는 테스트를 먼저 작성
- Green: 최소한의 코드로 테스트 통과
- Refactor: 코드 정리
TDD 예제: clamp 함수
요구사항: clamp(value, min, max) — value가 min보다 작으면 min, max보다 크면 max, 그렇지 않으면 value 반환.
1단계: Red — 실패하는 테스트
TEST(ClampTest, ValueWithinRange) {
EXPECT_EQ(clamp(5, 0, 10), 5); // clamp 함수 아직 없음 → 컴파일 에러
}
2단계: Green — 최소 구현
int clamp(int value, int min_val, int max_val) {
if (value < min_val) return min_val;
if (value > max_val) return max_val;
return value;
}
TEST(ClampTest, ValueWithinRange) {
EXPECT_EQ(clamp(5, 0, 10), 5);
}
TEST(ClampTest, ValueBelowMin) {
EXPECT_EQ(clamp(-5, 0, 10), 0);
}
TEST(ClampTest, ValueAboveMax) {
EXPECT_EQ(clamp(15, 0, 10), 10);
}
3단계: Refactor — std::clamp 사용
#include <algorithm>
int clamp(int value, int min_val, int max_val) {
return std::clamp(value, min_val, max_val);
}
TDD 원칙
| 원칙 | 설명 |
|---|---|
| 테스트 먼저 | 구현 전에 테스트 작성 |
| 작은 단위 | 한 번에 하나의 동작만 검증 |
| 빠른 피드백 | 테스트는 수 초 내에 완료 |
| 독립적 | 테스트 간 의존성 없음 |
8. 실전 패턴
패턴 1: 경계값 테스트
빈 컨테이너, 크기 1, 매우 큰 크기 같은 경계 조건에서 버그가 자주 나므로, empty(), size(), max_size() 등을 명시적으로 검증하는 테스트를 두는 것이 좋습니다. vec.resize(vec.max_size())는 메모리 한계까지 쓰는 극단적인 경우로, 실제로는 스킵하거나 별도 환경에서만 돌리는 식으로 다룰 수 있습니다.
TEST(VectorTest, BoundaryConditions) {
std::vector<int> vec;
// 빈 벡터
EXPECT_TRUE(vec.empty());
EXPECT_EQ(vec.size(), 0);
// 하나 추가
vec.push_back(1);
EXPECT_FALSE(vec.empty());
EXPECT_EQ(vec.size(), 1);
}
패턴 2: 상태 전이 테스트
상태 머신은 IDLE → start() → RUNNING → stop() → STOPPED처럼 전이가 명확하므로, getState()를 호출해 “이 동작 후에는 이 상태여야 한다”를 EXPECT_EQ로 검증합니다. 잘못된 순서(예: STOPPED에서 start 호출)나 중복 호출 시 상태가 바뀌지 않는지도 테스트에 넣으면 회귀를 막는 데 도움이 됩니다.
enum class State { IDLE, RUNNING, STOPPED };
class Machine {
State state = State::IDLE;
public:
void start() {
if (state == State::IDLE) {
state = State::RUNNING;
}
}
void stop() {
if (state == State::RUNNING) {
state = State::STOPPED;
}
}
State getState() const { return state; }
};
TEST(MachineTest, StateTransitions) {
Machine m;
EXPECT_EQ(m.getState(), State::IDLE);
m.start();
EXPECT_EQ(m.getState(), State::RUNNING);
m.stop();
EXPECT_EQ(m.getState(), State::STOPPED);
}
TEST(MachineTest, IdempotentStopInIdle) {
Machine m;
m.stop(); // IDLE에서 stop 호출
EXPECT_EQ(m.getState(), State::IDLE); // 상태 유지
}
패턴 3: 에러 처리 테스트
#include <fstream>
class FileReader {
public:
std::string read(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file");
}
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
return content;
}
};
TEST(FileReaderTest, NonExistentFile) {
FileReader reader;
EXPECT_THROW(reader.read("nonexistent.txt"), std::runtime_error);
}
패턴 4: 성능 테스트 (주의)
단위 테스트에서 chrono로 시간을 재고 EXPECT_LT(duration.count(), 100)처럼 “이 작업이 100ms 이내여야 한다”를 검사할 수 있습니다. 다만 CI 환경에 따라 결과가 달라질 수 있으므로, 여유 있는 임계값을 두거나 “느린 환경에서는 스킵”하는 식으로 설계하는 것이 좋습니다. 정밀한 벤치마크는 Google Benchmark 등 전용 도구를 쓰는 편이 낫습니다.
TEST(PerformanceTest, LargeVector) {
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> vec(1000000);
for (int i = 0; i < 1000000; ++i) {
vec[i] = i;
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_LT(duration.count(), 100); // 100ms 이내
}
8. 프로덕션 패턴
패턴 1: 테스트 디렉터리 구조
project/
├── src/
│ ├── calculator.cpp
│ └── calculator.h
├── test/
│ ├── CMakeLists.txt
│ ├── test_calculator.cpp
│ └── test_main.cpp
└── CMakeLists.txt
패턴 2: 테스트와 프로덕션 코드 분리
add_executable(calculator_test
test/test_calculator.cpp
)
target_link_libraries(calculator_test PRIVATE
calculator_lib
gtest_main
)
target_include_directories(calculator_test PRIVATE
${CMAKE_SOURCE_DIR}/src
)
패턴 3: DISABLED_ 테스트
작업 중인 테스트는 DISABLED_ 접두사로 스킵. --gtest_also_run_disabled_tests로 비활성화된 테스트도 실행 가능.
패턴 4: 테스트 필터로 CI 최적화
--gtest_filter=SmokeTest.*로 스모크만 먼저 실행해 빠른 피드백을 얻을 수 있습니다.
패턴 5: SCOPED_TRACE로 디버깅
루프 내에서 SCOPED_TRACE("i = " + std::to_string(i))를 사용하면 실패 시 “i = 3” 같은 컨텍스트가 출력되어 디버깅이 쉬워집니다.
패턴 6: RecordProperty로 메타데이터
RecordProperty("key", value)로 테스트 메타데이터를 추가하면 --gtest_output=xml 리포트에 포함됩니다.
패턴 7: 테스트 태깅과 계층화 (Smoke / Unit / Integration)
SmokeTest, UnitTest, IntegrationTest 스위트로 구분해 --gtest_filter=SmokeTest.*로 스모크만 먼저 돌려 빠른 피드백을 얻을 수 있습니다.
패턴 8: XML 리포트로 CI 연동
./myapp_test --gtest_output=xml:report.xml로 JUnit 호환 XML을 생성해 GitHub Actions, Jenkins 등에 연동할 수 있습니다.
10. 모범 사례 (Best Practices)
테스트 이름 짓기
| 나쁜 예 | 좋은 예 |
|---|---|
Test1 | Add_PositiveNumbers_ReturnsSum |
testParse | Parse_EmptyString_ThrowsInvalidArgument |
check | ValidateOrder_ZeroAmount_Rejected |
형식: 메서드명_입력조건_기대결과 또는 동작_상황_결과. 테스트 이름만 봐도 “무엇을 검증하는지” 알 수 있어야 합니다.
EXPECT vs ASSERT 선택 가이드
// ✅ EXPECT: 여러 검증을 모두 실행해 보고 싶을 때
TEST(VectorTest, MultipleChecks) {
std::vector<int> v = {1, 2, 3};
EXPECT_EQ(v.size(), 3);
EXPECT_EQ(v[0], 1);
EXPECT_EQ(v[2], 3); // 위에서 실패해도 여기까지 실행됨
}
// ✅ ASSERT: 이 조건이 아니면 아래 검증이 무의미할 때
TEST(PointerTest, Dereference) {
auto* ptr = getPointer();
ASSERT_NE(ptr, nullptr); // null이면 역참조 시 크래시
EXPECT_EQ(ptr->value(), 42);
}
테스트 격리 (Isolation)
- 각 테스트는 독립적으로 실행 가능해야 합니다.
- 테스트 간 실행 순서에 의존하지 않습니다.
- 전역 변수·싱글톤은 픽스처에서 초기화하거나 Mock으로 대체합니다.
테스트 속도
- 단위 테스트 전체는 수 초 이내에 끝나야 합니다.
- 파일 I/O, 네트워크, DB 접근은 Mock으로 대체합니다.
- 느린 테스트는
DISABLED_또는*IntegrationTest로 분리해 선택 실행합니다.
한 테스트 한 개념 (Single Responsibility)
// ❌ 나쁜 예: 여러 개념을 한 테스트에
TEST(AccountTest, Everything) {
Account a(100);
a.deposit(50);
a.withdraw(30);
a.transfer(b, 20); // 너무 많은 검증
}
// ✅ 좋은 예: 하나의 동작만 검증
TEST(AccountTest, Deposit_IncreasesBalance) {
Account a(100);
a.deposit(50);
EXPECT_EQ(a.getBalance(), 150);
}
리팩토링 시 테스트 활용
리팩토링 전·후 모두 테스트가 통과하는지 확인합니다. 실패 시 동작이 바뀌었다는 신호입니다.
11. 자주 발생하는 오류와 해결
오류 1: “undefined reference to testing::InitGoogleTest”
원인: gtest_main을 링크하지 않았거나, gtest 라이브러리 링크 순서 문제.
해결:
target_link_libraries(myapp_test PRIVATE gtest_main)
직접 main을 작성하려면:
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
그리고 gtest만 링크 (gtest_main은 제외):
target_link_libraries(myapp_test PRIVATE gtest)
오류 2: “TEST_F using fixture class that is not a descendant of testing::Test”
원인: 픽스처 클래스가 ::testing::Test를 상속하지 않음.
해결:
class MyFixture : public ::testing::Test {
protected:
void SetUp() override { /* ... */ }
};
오류 3: “Multiple definition of main”
원인: gtest_main을 링크하면서 main() 함수도 직접 정의함.
해결: 둘 중 하나만 사용. gtest_main 사용 시 main 없이 TEST만 정의.
오류 4: “pthread 링크 에러” (Linux)
원인: gtest가 pthread를 사용하는데 링크하지 않음.
해결:
target_link_libraries(myapp_test PRIVATE gtest_main Threads::Threads)
또는:
g++ -std=c++17 test.cpp -lgtest -lgtest_main -lpthread -o test
오류 5: “EXPECT_EQ(actual, expected) 순서 혼동”
원인: EXPECT_EQ(실제값, 기대값) 순서를 바꾸면 실패 메시지가 “Expected: X, Actual: Y”로 표시됩니다. 실패 시 “Expected”가 기대값, “Actual”이 실제값입니다.
해결: 일관되게 EXPECT_EQ(actual, expected) 형태로 작성하고, 실패 메시지를 읽을 때 “Actual”이 예상과 다른 값인지 확인합니다.
오류 6: 부동소수점 비교 실패
원인: EXPECT_EQ(0.1 + 0.2, 0.3) — 부동소수점 오차로 실패.
해결:
EXPECT_NEAR(0.1 + 0.2, 0.3, 1e-9);
// 또는
EXPECT_DOUBLE_EQ(0.1 + 0.2, 0.3);
오류 7: “C++17 structured binding in INSTANTIATE_TEST_SUITE_P”
원인: 일부 구 컴파일러에서 auto [a, b, expected] = GetParam(); 지원 문제.
해결:
TEST_P(AddTest, Addition) {
auto params = GetParam();
int a = std::get<0>(params);
int b = std::get<1>(params);
int expected = std::get<2>(params);
EXPECT_EQ(add(a, b), expected);
}
오류 8: Death Test가 “failed”로 나와요
원인: assert가 NDEBUG 빌드에서 비활성화됨. 또는 정규식이 stderr와 불일치.
해결: cmake -DCMAKE_BUILD_TYPE=Debug ..로 Debug 빌드. 정규식 ""은 “출력 있음”으로 검증.
오류 9: “INSTANTIATE_TEST_CASE_P is deprecated”
해결: INSTANTIATE_TEST_CASE_P → INSTANTIATE_TEST_SUITE_P로 변경 (Google Test 1.10+).
오류 10: 테스트가 CI에서만 실패해요 (Flaky Test)
원인: 타이밍 의존, 파일 경로 차이, 환경 변수 차이, 비결정적 동작.
해결: std::random 대신 시드 고정, 파일 경로는 환경 변수로 조정, --gtest_repeat=10으로 재현성 확인.
// ❌ 비결정적: rand() 매번 다른 값
// ✅ 시드 고정: std::srand(42); 후 rand() 사용
정리
주요 Assertion: EXPECT_EQ(같음), EXPECT_NE(다름), EXPECT_LT/GT(크기), EXPECT_TRUE/FALSE(불린), EXPECT_THROW(예외), EXPECT_NEAR(부동소수점), EXPECT_DEATH(종료 검증).
핵심 원칙:
- 모든 public 함수 테스트
- 경계값 테스트 필수
- 예외 처리 테스트
- 테스트는 독립적으로
- 테스트 이름은 명확하게
구현 체크리스트
-
FetchContent또는 vcpkg로 gtest 설치 -
TEST/TEST_F로 테스트 작성 -
EXPECT_*vsASSERT_*구분 - 경계값 테스트 추가
- 예외 테스트 (
EXPECT_THROW) - Death Test (
EXPECT_DEATH) — assert/abort 검증 - 픽스처로 공통 초기화
- 파라미터화 테스트로 중복 제거
- 테스트 이름 규칙 (
메서드_입력_기대결과) - CI에
ctest연동
참고: Google Test 공식 문서, Assertions Reference, Death Tests
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Google Mock | “DB 없이 테스트하고 싶어요” Mock 객체로 의존성 분리
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
- C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기
이 글에서 다루는 키워드 (관련 검색어)
C++ 단위 테스트, Google Test 설치, gtest 사용법, EXPECT_EQ ASSERT_EQ, C++ 테스트 코드, FetchContent gtest, vcpkg gtest, 테스트 픽스처, 파라미터화 테스트, C++ TDD 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Google Test 프레임워크로 단위 테스트를 작성하고, 테스트 픽스처를 사용하며, 실전에서 테스트 가능한 코드를 만드는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
Q. Google Test 설치가 실패하거나 링크 에러가 나요.
A. FetchContent 사용 시 GIT_TAG가 올바른지 확인하세요. release-1.12.1 등 안정 버전을 쓰고, CMake configure 후 build 폴더에 googletest가 내려받아졌는지 확인합니다. vcpkg 사용 시 vcpkg install gtest 후 CMake에서 CMAKE_TOOLCHAIN_FILE을 지정해야 합니다. “undefined reference to testing::InitGoogleTest” 같은 에러는 gtest_main을 링크하지 않았을 때 자주 나오므로, target_link_libraries(테스트타겟 PRIVATE gtest_main)을 추가하세요.
Q. CI(예: GitHub Actions)에서 테스트를 어떻게 돌리나요?
A. CI 스크립트에서 cmake --build build 후 ctest --test-dir build 또는 ./build/myapp_test처럼 테스트 실행 파일을 실행하면 됩니다. add_test()로 CTest에 등록해 두면 ctest 한 번으로 모든 테스트를 돌릴 수 있습니다. GitHub Actions에서는 cmake -B build → cmake --build build → ctest --test-dir build --output-on-failure 순서로 넣으면 됩니다.
한 줄 요약: Google Test로 단위 테스트를 작성하고 ASSERT/EXPECT로 동작을 검증할 수 있습니다. 다음으로 Google Mock(#18-2)를 읽어보면 좋습니다.
다음 글: [C++ 실전 가이드 #18-2] Google Mock으로 의존성 모킹하기
이전 글: [C++ 실전 가이드 #17-2] 패키지 매니저: vcpkg와 Conan으로 의존성 관리하기
관련 글
- C++ Google Mock |
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
- C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열
- C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing
- C++ 테스트 전략 완벽 가이드 | 단위·통합·E2E·모킹·프로덕션 패턴 [#55-7]