C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ

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가 같은 그림을 완성합니다.

문제 상황: calculatea + 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

목차

  1. Google Test 시작하기
  2. 기본 테스트 작성
  3. 테스트 픽스처
  4. 파라미터화 테스트
  5. Death Test (죽음 테스트)
  6. 테스트 패턴 (AAA, Given-When-Then)
  7. TDD 실전
  8. 실전 패턴
  9. 프로덕션 패턴
  10. 모범 사례 (Best Practices)
  11. 자주 발생하는 오류와 해결

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::stringEXPECT_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) 조합을 Valuesmake_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
  1. Red: 실패하는 테스트를 먼저 작성
  2. Green: 최소한의 코드로 테스트 통과
  3. 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)

테스트 이름 짓기

나쁜 예좋은 예
Test1Add_PositiveNumbers_ReturnsSum
testParseParse_EmptyString_ThrowsInvalidArgument
checkValidateOrder_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_PINSTANTIATE_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(종료 검증).

핵심 원칙:

  1. 모든 public 함수 테스트
  2. 경계값 테스트 필수
  3. 예외 처리 테스트
  4. 테스트는 독립적으로
  5. 테스트 이름은 명확하게

구현 체크리스트

  • FetchContent 또는 vcpkg로 gtest 설치
  • TEST / TEST_F로 테스트 작성
  • EXPECT_* vs ASSERT_* 구분
  • 경계값 테스트 추가
  • 예외 테스트 (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 buildctest --test-dir build 또는 ./build/myapp_test처럼 테스트 실행 파일을 실행하면 됩니다. add_test()로 CTest에 등록해 두면 ctest 한 번으로 모든 테스트를 돌릴 수 있습니다. GitHub Actions에서는 cmake -B buildcmake --build buildctest --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]