C++ 테스트 전략 완벽 가이드 | 단위·통합·E2E·모킹·프로덕션 패턴 [#55-7]
이 글의 핵심
플러그인·동적 라이브러리·엔터프라이즈 C++ 테스트 전략: 문제 시나리오, GTest·GMock 단위/통합/E2E 예제, 모킹 패턴, 자주 발생하는 에러, 모범 사례, CI/CD 프로덕션 패턴까지 실전 코드로 정리합니다.
들어가며: “리팩터링할 때마다 뭔가 깨질까 봐 두렵다"
"플러그인 하나 수정했는데 호스트 앱이 크래시해요”
플러그인 시스템, 동적 라이브러리, 엔터프라이즈 C++ 프로젝트에서는 코드 변경 시 부작용이 치명적입니다. “private 멤버 하나 추가했을 뿐”인데 ABI가 깨져 기존 앱이 죽거나, “DB 연결 로직만 바꿨는데” 결제가 실패하는 일이 발생합니다. 비유하면 “다리 한 칸만 수리했는데 전체 다리가 무너지는” 것처럼, 테스트 없이는 리팩터링이 두렵고 배포가 불안합니다.
테스트 전략은 단위 테스트(함수·클래스 단위), 통합 테스트(모듈·DB·API 연동), E2E 테스트(전체 시나리오), 모킹(외부 의존성 가짜 대체)을 계층적으로 구성해, 빠르고 안정적으로 품질을 검증하는 체계입니다.
문제의 핵심:
- 단위 테스트만 있으면 “각 부분은 맞는데 합치면 안 된다”는 통합 버그를 놓칩니다.
- 통합 테스트만 있으면 느리고, 실패 시 “어디가 문제인지” 좁히기 어렵습니다.
- E2E만 의존하면 10분 걸리는 테스트에 개발 속도가 묶입니다.
- 외부 DB·API·파일 없이 테스트하려면 모킹이 필수입니다.
이 글에서 다루는 것:
- 문제 시나리오: 테스트 전략이 필요한 실제 상황
- 완전한 테스트 전략 예제: 단위·통합·E2E·모킹 각각 동작하는 코드
- 자주 발생하는 에러와 해결법
- 모범 사례와 프로덕션 패턴
요구 환경: C++17 이상, GTest 1.12+, GMock
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오
- 테스트 피라미드와 전략 개요
- 단위 테스트 완전 예제
- 통합 테스트 완전 예제
- E2E 테스트 완전 예제
- 모킹 전략과 예제
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 정리
1. 문제 시나리오
시나리오 1: 플러그인 업데이트 후 호스트 크래시
상황: 이미지 필터 플러그인에 캐시 로직을 추가했습니다. 단독으로는 잘 동작하는데, 호스트 앱과 함께 실행하면 세그멘테이션 폴트가 발생합니다.
원인: 단위 테스트는 플러그인만 검증했고, 호스트-플러그인 경계(ABI, 메모리 소유권)는 검증하지 않았습니다.
해결: 통합 테스트에서 호스트가 플러그인을 로드하고 실제 API를 호출하는 시나리오를 추가합니다. ABI 호환성 검증을 CI에 포함합니다.
시나리오 2: DB 연결 실패 시 결제 로직 검증 불가
상황: 결제 서비스의 “잔액 부족 시 거부” 로직을 테스트하려 합니다. 실제 DB를 띄우면 느리고, 테스트 DB가 다운되면 전체 CI가 실패합니다.
원인: 외부 의존성(DB)이 테스트를 지배하고 있습니다.
해결: MockDatabase로 getBalance()가 0을 반환하도록 설정하고, 결제 서비스만 단위 테스트합니다. DB 연결은 통합 테스트에서 별도 검증합니다.
시나리오 3: “로컬에서는 되는데 CI에서만 실패”
상황: 로컬에서 ./run_tests는 통과하는데, GitHub Actions에서는 “cannot open shared object file”로 실패합니다.
원인: 로컬에는 LD_LIBRARY_PATH가 설정되어 있지만, CI 환경에는 없습니다. 환경 차이가 테스트 안정성을 해칩니다.
해결: CI에서 동일한 환경을 구성합니다. RPATH 설정, Docker 이미지 사용, 테스트 실행 전 LD_LIBRARY_PATH 설정을 CI 스크립트에 명시합니다.
시나리오 4: 테스트가 30분 걸려서 PR마다 대기
상황: 500개 테스트가 있고, 대부분이 실제 DB·네트워크·파일 시스템을 사용합니다. PR 하나에 30분이 걸립니다.
원인: 테스트 피라미드가 뒤집혀 있습니다. 느린 통합/E2E 테스트가 많고, 빠른 단위 테스트가 적습니다.
해결: 단위 테스트 비율을 높이고, 모킹으로 외부 의존성을 제거합니다. 통합/E2E는 병렬화하거나, PR에서는 핵심만 돌리고 나머지는 메인 브랜치에서 실행합니다.
시나리오 5: “테스트는 있는데 리팩터링 시 신뢰가 안 간다”
상황: 테스트가 100개 있는데, 리팩터링 후 모두 통과해도 “실제로는 뭔가 깨졌을 수 있다”는 불안이 있습니다.
원인: 테스트가 구현에 묶여 있거나, 경계 케이스를 검증하지 않습니다. “호출됐는지”만 확인하고 “결과가 맞는지”는 검증하지 않는 경우가 많습니다.
해결: 행위 검증과 상태 검증을 함께 사용합니다. Mock 호출 횟수뿐 아니라, 최종 반환값·객체 상태를 EXPECT_EQ로 검증합니다. 경계값(0, 음수, 빈 문자열, nullptr) 테스트를 추가합니다.
테스트 전략 의사결정 흐름
flowchart TB
subgraph input[입력]
Q1[외부 의존성 있음?]
Q2[모듈 간 연동 검증 필요?]
Q3[전체 시나리오 검증 필요?]
end
subgraph decision[의사결정]
D1{단위 테스트}
D2{통합 테스트}
D3{E2E 테스트}
end
subgraph result[선택]
R1[모킹 + 단위]
R2[실제 DB/API + 통합]
R3[전체 앱 + E2E]
end
Q1 --> D1
Q2 --> D2
Q3 --> D3
D1 --> R1
D2 --> R2
D3 --> R3
2. 테스트 피라미드와 전략 개요
테스트 피라미드
flowchart TB
subgraph pyramid[테스트 피라미드]
E2E["E2E (소수)\n전체 시나리오\n느림"]
INT["통합 (중간)\n모듈 연동\n중간 속도"]
UNIT["단위 (다수)\n함수/클래스\n빠름"]
end
UNIT --> INT
INT --> E2E
| 계층 | 목적 | 속도 | 의존성 | 비율(권장) |
|---|---|---|---|---|
| 단위 | 함수·클래스 로직 검증 | 빠름 (ms) | 모킹 | 70% |
| 통합 | 모듈·DB·API 연동 검증 | 중간 (초) | 실제/스텁 | 20% |
| E2E | 전체 시나리오 검증 | 느림 (분) | 전체 환경 | 10% |
계층별 전략 요약
단위: GTest + GMock, 외부 의존성 Mock, 빠른 피드백
통합: 실제 DB/파일/플러그인 로드, 경계 검증
E2E: 전체 앱 실행, 사용자 시나리오, 스모크 테스트
3. 단위 테스트 완전 예제
3.1 기본 단위 테스트 (GTest)
대상: 순수 함수, 유틸리티 클래스. 외부 의존성 없음.
// calculator.hpp
#pragma once
int add(int a, int b);
int divide(int a, int b); // b==0이면 예외
// calculator.cpp
#include "calculator.hpp"
#include <stdexcept>
int add(int a, int b) { return a + b; }
int divide(int a, int b) {
if (b == 0) throw std::invalid_argument("divide by zero");
return a / b;
}
// test_calculator.cpp
#include <gtest/gtest.h>
#include "calculator.hpp"
TEST(CalculatorTest, AddPositiveNumbers) {
EXPECT_EQ(add(2, 3), 5);
}
TEST(CalculatorTest, AddNegativeNumbers) {
EXPECT_EQ(add(-1, -2), -3);
}
TEST(CalculatorTest, DivideNormal) {
EXPECT_EQ(divide(10, 2), 5);
}
TEST(CalculatorTest, DivideByZeroThrows) {
EXPECT_THROW(divide(10, 0), std::invalid_argument);
}
3.2 파라미터화 테스트 (경계값)
#include <gtest/gtest.h>
#include "calculator.hpp"
struct AddParams {
int a, b, expected;
};
class CalculatorParamTest : public ::testing::TestWithParam<AddParams> {};
TEST_P(CalculatorParamTest, Add) {
const auto& p = GetParam();
EXPECT_EQ(add(p.a, p.b), p.expected);
}
INSTANTIATE_TEST_SUITE_P(
AddCases,
CalculatorParamTest,
::testing::Values(
AddParams{0, 0, 0},
AddParams{1, 1, 2},
AddParams{-1, 1, 0},
AddParams{INT_MAX, 0, INT_MAX}
)
);
3.3 픽스처를 사용한 단위 테스트
#include <gtest/gtest.h>
#include <string>
#include <vector>
class StringProcessor {
public:
std::string toUpper(const std::string& s) const;
std::vector<std::string> split(const std::string& s, char delim) const;
};
class StringProcessorTest : public ::testing::Test {
protected:
void SetUp() override {
processor = std::make_unique<StringProcessor>();
}
std::unique_ptr<StringProcessor> processor;
};
TEST_F(StringProcessorTest, ToUpperEmpty) {
EXPECT_EQ(processor->toUpper(""), "");
}
TEST_F(StringProcessorTest, ToUpperNormal) {
EXPECT_EQ(processor->toUpper("hello"), "HELLO");
}
TEST_F(StringProcessorTest, SplitByComma) {
auto result = processor->split("a,b,c", ',');
ASSERT_EQ(result.size(), 3u);
EXPECT_EQ(result[0], "a");
EXPECT_EQ(result[1], "b");
EXPECT_EQ(result[2], "c");
}
4. 통합 테스트 완전 예제
4.1 플러그인 로드 통합 테스트
대상: 호스트가 동적 라이브러리를 로드하고, C API를 호출하는 경계.
// plugin_host.hpp
#pragma once
#include <string>
#include <memory>
class PluginHost {
public:
bool loadPlugin(const std::string& path);
int callPluginProcess(const void* input, size_t inLen, void* output, size_t outLen);
void unloadPlugin();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
// test_plugin_integration.cpp
#include <gtest/gtest.h>
#include "plugin_host.hpp"
#include <filesystem>
#include <cstring>
class PluginIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
host = std::make_unique<PluginHost>();
pluginPath = findTestPlugin();
}
std::string findTestPlugin() {
auto base = std::filesystem::path(BINARY_DIR) / "plugins";
for (const auto& e : std::filesystem::directory_iterator(base)) {
if (e.path().extension() == ".so" || e.path().extension() == ".dll")
return e.path().string();
}
return "";
}
std::unique_ptr<PluginHost> host;
std::string pluginPath;
};
TEST_F(PluginIntegrationTest, LoadAndProcess) {
if (pluginPath.empty()) {
GTEST_SKIP() << "Test plugin not found";
}
ASSERT_TRUE(host->loadPlugin(pluginPath));
char input[] = "test";
char output[256] = {};
int result = host->callPluginProcess(input, 4, output, sizeof(output));
EXPECT_GE(result, 0);
host->unloadPlugin();
}
4.2 파일 시스템 통합 테스트
// file_service.hpp
#pragma once
#include <string>
#include <vector>
class FileService {
public:
std::vector<std::string> listFiles(const std::string& dir) const;
bool writeFile(const std::string& path, const std::string& content) const;
};
// test_file_integration.cpp
#include <gtest/gtest.h>
#include "file_service.hpp"
#include <filesystem>
#include <fstream>
class FileIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
testDir = std::filesystem::temp_directory_path() / "file_service_test";
std::filesystem::create_directories(testDir);
service = std::make_unique<FileService>();
}
void TearDown() override {
std::filesystem::remove_all(testDir);
}
std::filesystem::path testDir;
std::unique_ptr<FileService> service;
};
TEST_F(FileIntegrationTest, ListFiles) {
std::ofstream((testDir / "a.txt").string()) << "a";
std::ofstream((testDir / "b.txt").string()) << "b";
auto files = service->listFiles(testDir.string());
ASSERT_EQ(files.size(), 2u);
}
4.3 통합 테스트 시퀀스 다이어그램
sequenceDiagram
participant T as 테스트
participant H as PluginHost
participant P as Plugin(.so)
T->>H: loadPlugin(path)
H->>P: dlopen + dlsym
P-->>H: handle
H-->>T: true
T->>H: callPluginProcess(...)
H->>P: process(input, output)
P-->>H: result
H-->>T: result
T->>H: unloadPlugin()
H->>P: dlclose
5. E2E 테스트 완전 예제
5.1 전체 앱 실행 E2E
대상: 실행 파일을 서브프로세스로 띄우고, 입력·출력·종료 코드를 검증.
// test_e2e_app.cpp
#include <gtest/gtest.h>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>
std::string runApp(const std::string& args, int& exitCode) {
std::string cmd = "./myapp " + args + " 2>&1";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
exitCode = -1;
return "";
}
std::ostringstream out;
char buf[256];
while (fgets(buf, sizeof(buf), pipe)) {
out << buf;
}
exitCode = pclose(pipe);
return out.str();
}
TEST(E2ETest, AppHelp) {
int exitCode = 0;
auto output = runApp("--help", exitCode);
EXPECT_EQ(exitCode, 0);
EXPECT_TRUE(output.find("Usage") != std::string::npos);
}
TEST(E2ETest, AppProcessFile) {
int exitCode = 0;
auto output = runApp("--input test.txt --output out.txt", exitCode);
EXPECT_EQ(exitCode, 0);
}
5.2 E2E 테스트 전략
flowchart LR
subgraph e2e[E2E 범위]
A[앱 시작] --> B[설정 로드]
B --> C[플러그인 로드]
C --> D[입력 처리]
D --> E[출력 생성]
E --> F[정상 종료]
end
- 스모크 테스트:
--help,--version등 빠른 경로만 검증 - 핵심 시나리오: 1~2개 대표 플로우 (예: 파일 처리)
- 실패 경로: 잘못된 인자, 누락된 파일 등
6. 모킹 전략과 예제
6.1 인터페이스 기반 Mock
// database.hpp
#pragma once
#include <string>
#include <memory>
struct User {
std::string id;
std::string name;
int balance;
};
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual User* findUser(const std::string& id) = 0;
virtual bool saveUser(const User& user) = 0;
};
// mock_database.hpp
#pragma once
#include "database.hpp"
#include <gmock/gmock.h>
class MockDatabase : public IDatabase {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
MOCK_METHOD(bool, saveUser, (const User&), (override));
};
6.2 PaymentService 단위 테스트 (Mock 사용)
// payment_service.hpp
#pragma once
#include "database.hpp"
class PaymentService {
public:
explicit PaymentService(IDatabase* db) : db_(db) {}
bool charge(const std::string& userId, int amount);
private:
IDatabase* db_;
};
// payment_service.cpp
#include "payment_service.hpp"
bool PaymentService::charge(const std::string& userId, int amount) {
auto* user = db_->findUser(userId);
if (!user) return false;
if (user->balance < amount) return false;
user->balance -= amount;
return db_->saveUser(*user);
}
// test_payment_service.cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "payment_service.hpp"
#include "mock_database.hpp"
using ::testing::Return;
using ::testing::_;
TEST(PaymentServiceTest, ChargeSuccess) {
MockDatabase mockDb;
PaymentService service(&mockDb);
User user{"u1", "Alice", 100};
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
EXPECT_CALL(mockDb, saveUser(_)).WillOnce(Return(true));
EXPECT_TRUE(service.charge("u1", 50));
}
TEST(PaymentServiceTest, ChargeFailure_InsufficientBalance) {
MockDatabase mockDb;
PaymentService service(&mockDb);
User user{"u1", "Alice", 30};
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
EXPECT_CALL(mockDb, saveUser(_)).Times(0);
EXPECT_FALSE(service.charge("u1", 50));
}
TEST(PaymentServiceTest, ChargeFailure_UserNotFound) {
MockDatabase mockDb;
PaymentService service(&mockDb);
EXPECT_CALL(mockDb, findUser("unknown")).WillOnce(Return(nullptr));
EXPECT_CALL(mockDb, saveUser(_)).Times(0);
EXPECT_FALSE(service.charge("unknown", 50));
}
6.3 행위 검증 + 상태 검증
TEST(PaymentServiceTest, ChargeUpdatesBalance) {
MockDatabase mockDb;
PaymentService service(&mockDb);
User user{"u1", "Alice", 100};
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
EXPECT_CALL(mockDb, saveUser(::testing::Field(&User::balance, 50)))
.WillOnce(Return(true));
EXPECT_TRUE(service.charge("u1", 50));
EXPECT_EQ(user.balance, 50); // 상태 검증
}
6.4 호출 순서 검증 (InSequence)
#include <gmock/gmock.h>
using ::testing::InSequence;
using ::testing::Return;
TEST(OrderTest, FindBeforeSave) {
MockDatabase mockDb;
User user{"u1", "Alice", 100};
{
InSequence seq;
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
EXPECT_CALL(mockDb, saveUser(_)).WillOnce(Return(true));
}
PaymentService service(&mockDb);
service.charge("u1", 50);
}
6.5 NiceMock vs StrictMock
// NiceMock: 예상하지 않은 호출을 경고만 하고 실패하지 않음
::testing::NiceMock<MockDatabase> niceDb;
// StrictMock: 예상하지 않은 호출이 있으면 즉시 실패
::testing::StrictMock<MockDatabase> strictDb;
7. 자주 발생하는 에러와 해결법
에러 1: “undefined reference to testing::InitGoogleTest”
증상: 링크 시 undefined reference to testing::InitGoogleTest
원인: GTest 라이브러리를 링크하지 않음
해결:
# CMakeLists.txt
find_package(GTest REQUIRED)
target_link_libraries(my_test PRIVATE GTest::gtest_main)
# 직접 링크
g++ -o test test.cpp -lgtest -lgtest_main -lpthread
에러 2: “EXPECT_CALL이 만족되지 않음”
증상: Actual: never called 또는 Expected: to be called once
원인: Mock 메서드가 호출되지 않았거나, 인자 매칭이 맞지 않음
해결:
// ❌ 잘못된 예 — 정확한 인자만 매칭
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&user));
// findUser("alice ") 호출 시 매칭 안 됨
// ✅ 올바른 예 — 유연한 매칭
EXPECT_CALL(mockDb, findUser(::testing::_)).WillOnce(Return(&user));
// 또는
EXPECT_CALL(mockDb, findUser(::testing::StartsWith("alice"))).WillOnce(Return(&user));
에러 3: “Mock 객체가 테스트 후에 소멸될 때 실패”
증상: 테스트는 통과하는데, 픽스처 소멸 시 “uninteresting call” 에러
원인: StrictMock 사용 시 소멸자 등 예상하지 않은 호출 발생
해결:
// ✅ NiceMock 사용 또는
::testing::NiceMock<MockDatabase> mockDb;
// ✅ 또는 AllowUninterestingCalls
EXPECT_CALL(mockDb, findUser(_)).WillRepeatedly(Return(nullptr));
에러 4: “dangling pointer” — Mock이 반환한 포인터가 무효화
증상: Return(&localUser) 후 localUser가 스택에서 사라졌는데, 테스트 대상이 나중에 참조
해결:
// ❌ 위험 — 로컬 변수 주소 반환
User user{"u1", "Alice", 100};
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
// charge() 내부에서 user 참조 시 스택이 무효화될 수 있음
// ✅ 안전 — 테스트 범위 내에서 생존하는 객체 사용
class PaymentServiceTest : public ::testing::Test {
protected:
User user{"u1", "Alice", 100}; // 멤버로 유지
};
TEST_F(PaymentServiceTest, Charge) {
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
// ...
}
에러 5: “cannot open shared object file” (통합 테스트)
증상: 플러그인 로드 통합 테스트에서 dlopen 실패
원인: CI/다른 환경에서 LD_LIBRARY_PATH 미설정
해결:
# CMake: 테스트 실행 전 환경 설정
add_test(NAME plugin_integration COMMAND plugin_integration_test)
set_tests_properties(plugin_integration PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=$<TARGET_FILE_DIR:test_plugin>:${CMAKE_BINARY_DIR}"
)
# 또는 RPATH로 실행 파일 기준 경로 지정
set_target_properties(plugin_integration_test PROPERTIES
INSTALL_RPATH "$ORIGIN/../plugins"
BUILD_WITH_INSTALL_RPATH TRUE
)
에러 6: “테스트 순서에 따라 결과가 달라짐”
증상: 단독 실행 시 통과, 전체 실행 시 실패
원인: 전역 상태, 정적 변수, 싱글톤 공유
해결:
- 각 테스트에서 독립적인 픽스처 사용
- 전역 상태를 초기화하는
TearDown추가 - 테스트 간 순서에 의존하지 않도록 설계
class GlobalStateTest : public ::testing::Test {
protected:
void TearDown() override {
resetGlobalState(); // 다음 테스트를 위해 초기화
}
};
에러 7: “GMock MOCK_METHOD 매크로 오류”
증상: MOCK_METHOD 사용 시 컴파일 에러
원인: 인자 목록을 괄호로 묶지 않음 (오버로드된 함수)
해결:
// ❌ 잘못된 예
MOCK_METHOD(User*, findUser, const std::string&, override);
// ✅ 올바른 예 — 인자를 괄호로 묶음
MOCK_METHOD(User*, findUser, (const std::string&), (override));
에러 8: “통합 테스트가 로컬에서만 통과”
증상: 로컬에서는 되는데 CI에서 “파일 없음”, “권한 없음”
원인: 절대 경로, 로컬 전용 디렉터리 사용
해결:
// ✅ 상대 경로 또는 환경 변수
std::string getTestDataDir() {
const char* env = std::getenv("TEST_DATA_DIR");
if (env) return env;
return std::filesystem::path(BINARY_DIR) / "testdata";
}
8. 모범 사례
1. 테스트는 독립적이어야 한다
각 테스트는 다른 테스트에 의존하지 않고 단독으로 실행 가능해야 합니다. 공유 상태를 피하고, SetUp/TearDown으로 초기화합니다.
2. 테스트 이름은 행위를 설명한다
// ❌ 나쁜 예
TEST(Test1, Test) {}
TEST(Payment, Test2) {}
// ✅ 좋은 예
TEST(PaymentServiceTest, ChargeSucceedsWhenBalanceSufficient) {}
TEST(PaymentServiceTest, ChargeFailsWhenUserNotFound) {}
3. Arrange-Act-Assert 패턴
TEST(Example, Pattern) {
// Arrange: 준비
MockDatabase mockDb;
User user{"u1", "Alice", 100};
EXPECT_CALL(mockDb, findUser("u1")).WillOnce(Return(&user));
// Act: 실행
PaymentService service(&mockDb);
bool result = service.charge("u1", 50);
// Assert: 검증
EXPECT_TRUE(result);
}
4. 한 테스트에 한 가지 검증
한 테스트가 여러 시나리오를 검증하면, 실패 시 원인 파악이 어렵습니다. 하나의 동작·경로만 검증합니다.
5. 경계값·예외 경로 테스트
// 0, 음수, 빈 문자열, nullptr, INT_MAX 등
TEST(CalculatorTest, DivideByZero) { ... }
TEST(StringTest, EmptyInput) { ... }
TEST(PaymentTest, UserNotFound) { ... }
6. Mock은 인터페이스에만
구체 클래스가 아닌 추상 인터페이스에 Mock을 구현합니다. 의존성 주입으로 테스트 시 Mock을 주입합니다.
7. 통합 테스트는 격리된 환경에서
임시 디렉터리, 테스트 전용 DB, Mock 서버 등을 사용해 다른 테스트·실제 환경과 격리합니다.
9. 프로덕션 패턴
패턴 1: CMake 테스트 구조
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
enable_testing()
# 단위 테스트
add_executable(unit_tests
test_calculator.cpp
test_payment_service.cpp
)
target_link_libraries(unit_tests PRIVATE GTest::gtest_main gmock)
add_test(NAME unit_tests COMMAND unit_tests)
# 통합 테스트 (플러그인 경로 설정)
add_executable(integration_tests test_plugin_integration.cpp)
target_compile_definitions(integration_tests PRIVATE BINARY_DIR="${CMAKE_BINARY_DIR}")
target_link_libraries(integration_tests PRIVATE GTest::gtest_main)
add_test(NAME integration_tests COMMAND integration_tests)
set_tests_properties(integration_tests PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins"
)
패턴 2: CI 파이프라인 (GitHub Actions)
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-cpp@v1
with:
compiler: gcc
version: 12
- run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- run: cmake --build build
- run: cd build && ctest --output-on-failure -R unit_tests
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- run: cmake -B build && cmake --build build
- run: cd build && ctest --output-on-failure -R integration_tests
패턴 3: 커버리지 수집
# 커버리지 활성화
option(ENABLE_COVERAGE "Enable coverage" OFF)
if(ENABLE_COVERAGE)
target_compile_options(unit_tests PRIVATE --coverage)
target_link_options(unit_tests PRIVATE --coverage)
endif()
# 빌드 및 커버리지
cmake -B build -DENABLE_COVERAGE=ON
cmake --build build
cd build && ctest
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_html
패턴 4: 테스트 태깅 (빠른/느린 분리)
// 빠른 단위 테스트
TEST(CalculatorTest, Add) { ... }
// 느린 통합 테스트 — CI에서 선택 실행
class IntegrationTest : public ::testing::Test {
static bool isSlowTestEnabled() {
return std::getenv("RUN_SLOW_TESTS") != nullptr;
}
};
TEST_F(IntegrationTest, PluginLoad) {
if (!isSlowTestEnabled()) {
GTEST_SKIP() << "Set RUN_SLOW_TESTS=1 to run";
}
// ...
}
패턴 5: 테스트 데이터 관리
testdata/
plugins/
libtest_filter.so
fixtures/
sample_input.txt
expected_output.txt
std::string loadFixture(const std::string& name) {
auto path = std::filesystem::path(TEST_DATA_DIR) / "fixtures" / name;
std::ifstream f(path);
return std::string(std::istreambuf_iterator<char>(f), {});
}
패턴 6: 플래키 테스트 재시도
// 불안정한 통합 테스트 (네트워크 등) — 재시도
TEST(NetworkTest, Connect) {
for (int i = 0; i < 3; ++i) {
if (tryConnect()) return;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
FAIL() << "Connection failed after 3 retries";
}
10. 정리
| 계층 | 도구 | 목적 | 비율 |
|---|---|---|---|
| 단위 | GTest, GMock | 함수·클래스 로직, Mock으로 격리 | 70% |
| 통합 | GTest, 실제 DB/파일/플러그인 | 모듈 연동, 경계 검증 | 20% |
| E2E | GTest, 서브프로세스 | 전체 시나리오, 스모크 | 10% |
핵심 원칙:
- 테스트 피라미드를 지키고, 단위 테스트 비율을 높인다.
- 모킹으로 외부 의존성을 제거해 빠른 피드백을 얻는다.
- 통합 테스트로 플러그인·DB·파일 경계를 검증한다.
- E2E는 핵심 시나리오만, 스모크 수준으로 유지한다.
- CI에서 계층별로 분리 실행하고, 커버리지를 수집한다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 플러그인 시스템, 동적 라이브러리, 엔터프라이즈 C++ 프로젝트의 테스트 전략 수립 시 활용합니다. 본문의 문제 시나리오와 계층별 예제를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 단위 테스트 GTest(#18-1), 모킹 GMock(#18-2), 플러그인 시스템(#55-2)을 먼저 읽으면 이해가 쉽습니다.
Q. 프로덕션에서 테스트 커버리지는 얼마나 맞추나요?
A. 핵심 비즈니스 로직은 80% 이상, 플러그인 경계·에러 처리 경로는 90% 이상을 권장합니다. 본문의 프로덕션 패턴을 참고하세요.
참고 자료
- Google Test 공식 문서
- Google Mock 공식 문서
- C++ 시리즈 #18-1: 단위 테스트 GTest
- C++ 시리즈 #18-2: 모킹 GMock
- C++ 시리즈 #55-2: 플러그인 시스템
구현 체크리스트
테스트 전략 도입 시 확인할 항목:
- GTest/GMock 설치 및 CMake 연동
- 단위 테스트: 순수 로직, Mock으로 외부 의존성 제거
- 통합 테스트: 플러그인 로드, 파일, DB 연동
- E2E 테스트: 스모크 시나리오 1~2개
-
LD_LIBRARY_PATH/RPATH로 플러그인 경로 설정 - CI에서 단위 → 통합 순서 실행
- 커버리지 수집 (선택)
- 테스트 이름: 행위 설명
- Arrange-Act-Assert 패턴
- 경계값·예외 경로 테스트
한 줄 요약: 단위(모킹)·통합(실제 연동)·E2E(전체 시나리오)를 계층적으로 구성하고, CI에서 자동 실행해 안전한 리팩터링과 배포를 할 수 있습니다.
관련 글
- C++ 코드 커버리지 완벽 가이드 | gcov, lcov, Codecov 실전 활용
- C++ CI/CD GitHub Actions 완벽 가이드 | 워크플로·매트릭스·캐싱·아티팩트·배포