C++ Google Mock | "DB 없이 테스트하고 싶어요" Mock 객체로 의존성 분리
이 글의 핵심
C++ Google Mock에 대한 실전 가이드입니다.
들어가며: “데이터베이스 없이 테스트하고 싶어요”
외부 의존성 때문에 테스트가 어렵다
데이터베이스를 사용하는 코드를 테스트하고 싶었습니다. 하지만:
- 테스트마다 DB 연결 필요
- 느림 (수 초)
- 테스트 데이터 관리 어려움
요구 환경: Google Test + Google Mock(GMock). GTest와 함께 vcpkg(vcpkg install gtest), Conan, FetchContent로 포함. g++/Clang + C++14 이상. GMock은 GTest에 포함된 경우가 많으므로 같은 패키지로 설치하면 됩니다.
Mock(모킹—외부 의존을 가짜 구현으로 바꿔 테스트할 수 있게 하는 것)은 “외부 의존(DB, API, 파일)“을 가짜 구현으로 바꿔서, 실제 환경 없이 “이 메서드가 이렇게 불리면 이렇게 반환한다”를 검증하게 해 줍니다. 인터페이스 기반으로 설계하고 GMock으로 기대 호출을 정의해 두면, 단위 테스트가 빠르고 안정적으로 돌아가며, 나중에 구현을 바꿔도 테스트가 그대로 유효합니다.
문제의 코드: UserService::login이 실제 Database를 통해 findUser를 호출하므로, 테스트할 때마다 DB를 띄우고 데이터를 넣어야 하고 느려집니다. 테스트는 “UserService 로직”만 검증하고 싶은데, DB라는 외부 의존성이 섞여 있어 단위 테스트가 어렵습니다.
class UserService {
Database* db;
public:
bool login(const std::string& username, const std::string& password) {
auto user = db->findUser(username); // ❌ 실제 DB 접근
return user && user->password == password;
}
};
// 테스트하려면 실제 DB 필요...
Mock으로 해결: Database를 상속한 MockDatabase를 만들고, MOCK_METHOD로 findUser의 호출을 가로챕니다. 테스트에서는 EXPECT_CALL(mockDb, findUser(“alice”)).WillOnce(Return(&testUser))로 “alice로 호출되면 testUser를 반환하라”고 지정합니다. 이렇게 하면 실제 DB 없이 “findUser가 alice를 반환했을 때 login이 true를 반환하는지”만 검증할 수 있습니다.
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
};
TEST(UserServiceTest, LoginSuccess) {
MockDatabase mockDb;
UserService service(&mockDb);
User testUser{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice"))
.WillOnce(Return(&testUser));
EXPECT_TRUE(service.login("alice", "pass123"));
// DB 없이 테스트 완료!
}
이 글을 읽으면:
- Google Mock으로 Mock 객체를 만들 수 있습니다.
- 의존성을 분리하여 테스트할 수 있습니다.
- EXPECT_CALL로 동작을 검증할 수 있습니다.
- 실전에서 테스트 가능한 설계를 할 수 있습니다.
목차
1. Mock 기초
인터페이스 정의
Mock을 쓰려면 의존 대상이 추상 인터페이스(순수 가상 함수)로 되어 있어야 합니다. virtual ~Database() = default로 가상 소멸자를 두고, findUser, saveUser 등을 = 0으로 선언해 “구현은 파생 클래스에서” 하게 합니다. 이렇게 하면 테스트 시 MockDatabase처럼 가짜 구현으로 교체할 수 있고, 실제 코드에서는 RealDatabase를 주입하면 됩니다.
class Database {
public:
virtual ~Database() = default;
virtual User* findUser(const std::string& username) = 0;
virtual bool saveUser(const User& user) = 0;
virtual void deleteUser(int userId) = 0;
};
Mock 클래스 생성
MOCK_METHOD(반환타입, 메서드이름, (인자목록), (override)) 형식으로 Mock 메서드를 선언합니다. 인자가 여러 개면 (arg1, arg2)처럼 괄호로 묶고, override는 부모 가상 함수를 오버라이드한다는 표시입니다. GMock이 이 선언을 보고 “이 메서드가 어떻게 호출될지·무엇을 반환할지”를 EXPECT_CALL로 제어할 수 있게 해 줍니다.
#include <gmock/gmock.h>
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
MOCK_METHOD(bool, saveUser, (const User&), (override));
MOCK_METHOD(void, deleteUser, (int), (override));
};
기본 사용
EXPECT_CALL(mockDb, findUser(“alice”)).Times(1)은 “이 테스트 안에서 findUser(“alice”)가 정확히 1번 호출되어야 한다”는 기대를 등록합니다. 그 다음 mockDb.findUser(“alice”)를 호출하면 Mock이 그 호출을 기록하고, 테스트 종료 시 “1번 호출됐는지” 검사합니다. 호출 횟수가 맞지 않거나 아예 호출되지 않으면 테스트가 실패합니다.
TEST(DatabaseTest, FindUser) {
MockDatabase mockDb;
// findUser 호출 예상
EXPECT_CALL(mockDb, findUser("alice"))
.Times(1);
// 실제 호출
mockDb.findUser("alice");
}
2. EXPECT_CALL
호출 횟수
Times(n)은 정확히 n번, Times(AtLeast(2))는 2번 이상, Times(AtMost(3))는 최대 3번, Times(0)은 호출되면 안 됨을 의미합니다. “이 메서드는 한 번만 불려야 한다”거나 “반복문 안에서 여러 번 불릴 수 있다” 같은 시나리오를 명시적으로 검증할 수 있어, 로직이 예상대로 호출하는지 확인하는 데 유용합니다.
TEST(DatabaseTest, CallCounts) {
MockDatabase mockDb;
// 정확히 1번
EXPECT_CALL(mockDb, findUser("alice"))
.Times(1);
// 2번 이상
EXPECT_CALL(mockDb, findUser("bob"))
.Times(AtLeast(2));
// 최대 3번
EXPECT_CALL(mockDb, findUser("charlie"))
.Times(AtMost(3));
// 호출 안 됨
EXPECT_CALL(mockDb, findUser("dave"))
.Times(0);
}
호출 순서
InSequence seq를 선언한 뒤 나오는 EXPECT_CALL들은 선언된 순서대로 호출되어야 합니다. 즉 findUser → saveUser → deleteUser 순서가 아니면 테스트가 실패합니다. “먼저 사용자를 찾고, 수정한 뒤, 저장하고, 삭제한다” 같은 시나리오에서 순서가 바뀌면 버그인 경우에 유용합니다. **_**는 임의의 인자를 의미하는 매처입니다.
TEST(DatabaseTest, CallOrder) {
MockDatabase mockDb;
InSequence seq; // 순서 강제
EXPECT_CALL(mockDb, findUser("alice"));
EXPECT_CALL(mockDb, saveUser(_));
EXPECT_CALL(mockDb, deleteUser(_));
// 이 순서대로 호출되어야 함
}
3. 반환값 설정
WillOnce / WillRepeatedly
WillOnce(Return(&alice))는 “이 메서드가 한 번 호출될 때 &alice를 반환하라”는 뜻입니다. WillRepeatedly(Return(nullptr))는 “호출될 때마다(첫 호출 이후 포함) nullptr를 반환”합니다. 여러 번 호출되는 경우 WillOnce를 여러 개 나열하면 첫 번째 호출·두 번째 호출… 순서대로 다른 값을 반환하게 할 수 있습니다.
TEST(DatabaseTest, ReturnValues) {
MockDatabase mockDb;
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice"))
.WillOnce(Return(&alice)); // 첫 호출: alice 반환
EXPECT_CALL(mockDb, findUser("bob"))
.WillRepeatedly(Return(nullptr)); // 모든 호출: null 반환
}
여러 반환값
TEST(DatabaseTest, MultipleReturns) {
MockDatabase mockDb;
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice"))
.WillOnce(Return(nullptr)) // 첫 호출: null
.WillOnce(Return(&alice)) // 두 번째: alice
.WillRepeatedly(Return(nullptr)); // 이후: null
}
예외 던지기
WillOnce(Throw(예외))로 “이 호출 시 해당 예외를 던지게” 설정할 수 있습니다. 테스트에서는 EXPECT_THROW로 그 예외가 나오는지 검증합니다. 네트워크 끊김·DB 연결 실패 같은 에러 경로를 실제 DB 없이 시뮬레이션해서, 호출하는 쪽이 예외를 제대로 처리하는지 단위 테스트로 확인할 수 있습니다.
TEST(DatabaseTest, ThrowException) {
MockDatabase mockDb;
EXPECT_CALL(mockDb, findUser("invalid"))
.WillOnce(Throw(std::runtime_error("Connection failed")));
EXPECT_THROW(mockDb.findUser("invalid"), std::runtime_error);
}
Invoke로 콜백 실행
Invoke를 사용하면 Mock 호출 시 임의의 함수·람다를 실행할 수 있습니다. 반환값뿐 아니라 부수 효과(side effect)를 넣거나, 인자를 기반으로 동적 반환값을 만들 때 유용합니다.
TEST(DatabaseTest, InvokeCallback) {
MockDatabase mockDb;
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser(_))
.WillRepeatedly(Invoke([&alice](const std::string& name) -> User* {
return (name == "alice") ? &alice : nullptr;
}));
EXPECT_NE(mockDb.findUser("alice"), nullptr);
EXPECT_EQ(mockDb.findUser("bob"), nullptr);
}
4. 인자 매칭
기본 매처
_는 “어떤 인자든 허용”하는 매처입니다. Eq(“alice”)는 값이 “alice”와 같을 때만 매치하고, Ne(0), Gt(100)은 “0이 아님”, “100 초과”를 검사합니다. 인자 값이 테스트 시점에 고정이 아니거나, “0이 아닌 ID”처럼 조건만 중요할 때 매처를 쓰면 EXPECT_CALL을 유연하게 작성할 수 있습니다.
using ::testing::_;
using ::testing::Eq;
using ::testing::Ne;
using ::testing::Lt;
using ::testing::Gt;
TEST(DatabaseTest, Matchers) {
MockDatabase mockDb;
// 모든 인자
EXPECT_CALL(mockDb, findUser(_));
// 같음
EXPECT_CALL(mockDb, findUser(Eq("alice")));
// 다름
EXPECT_CALL(mockDb, deleteUser(Ne(0)));
// 크기 비교
EXPECT_CALL(mockDb, deleteUser(Gt(100)));
}
문자열 매처
using ::testing::StartsWith;
using ::testing::EndsWith;
using ::testing::HasSubstr;
TEST(DatabaseTest, StringMatchers) {
MockDatabase mockDb;
EXPECT_CALL(mockDb, findUser(StartsWith("admin_")));
EXPECT_CALL(mockDb, findUser(EndsWith("@gmail.com")));
EXPECT_CALL(mockDb, findUser(HasSubstr("test")));
}
커스텀 매처
MATCHER_P(이름, 파라미터, 설명)로 사용자 정의 매처를 만듭니다. arg는 EXPECT_CALL에 넘긴 인자(여기서는 User 객체)이고, return arg.name == name으로 “name 필드가 파라미터와 같은지” 검사합니다. saveUser(IsUserWithName(“alice”))는 “saveUser에 넘기는 User의 name이 alice일 때만” 이 호출에 매치됩니다. 복잡한 구조체나 비즈니스 조건을 한 번 정의해 두고 여러 테스트에서 재사용할 수 있습니다.
MATCHER_P(IsUserWithName, name, "") {
return arg.name == name;
}
TEST(DatabaseTest, CustomMatcher) {
MockDatabase mockDb;
EXPECT_CALL(mockDb, saveUser(IsUserWithName("alice")));
}
5. 실전 패턴
패턴 1: 의존성 주입
UserService가 Database*를 생성자 인자로 받으면, 테스트에서는 MockDatabase를 넘기고 실제 서비스에서는 RealDatabase를 넘길 수 있습니다. 이렇게 “의존성을 밖에서 주입”하는 방식을 의존성 주입(DI)이라고 합니다. 인터페이스 기반 설계와 함께 쓰면 Mock으로 외부 의존성을 완전히 제거한 단위 테스트가 가능합니다.
class UserService {
Database* db;
public:
UserService(Database* database) : db(database) {}
bool login(const std::string& username, const std::string& password) {
auto user = db->findUser(username);
return user && user->password == password;
}
};
TEST(UserServiceTest, LoginSuccess) {
MockDatabase mockDb;
UserService service(&mockDb);
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice"))
.WillOnce(Return(&alice));
EXPECT_TRUE(service.login("alice", "pass123"));
}
TEST(UserServiceTest, LoginFailure) {
MockDatabase mockDb;
UserService service(&mockDb);
EXPECT_CALL(mockDb, findUser("alice"))
.WillOnce(Return(nullptr));
EXPECT_FALSE(service.login("alice", "wrong"));
}
패턴 2: 네트워크 Mock
HttpClient를 인터페이스로 두고 MockHttpClient로 “특정 URL에 대해 이 JSON을 반환한다”고 설정하면, 실제 HTTP 요청 없이 ApiService가 응답을 어떻게 파싱·처리하는지 테스트할 수 있습니다. 외부 API가 느리거나 불안정할 때도 테스트가 빠르고 결정적으로 동작합니다.
class HttpClient {
public:
virtual ~HttpClient() = default;
virtual std::string get(const std::string& url) = 0;
virtual bool post(const std::string& url, const std::string& data) = 0;
};
class MockHttpClient : public HttpClient {
public:
MOCK_METHOD(std::string, get, (const std::string&), (override));
MOCK_METHOD(bool, post, (const std::string&, const std::string&), (override));
};
TEST(ApiServiceTest, FetchData) {
MockHttpClient mockHttp;
ApiService service(&mockHttp);
EXPECT_CALL(mockHttp, get("https://api.example.com/data"))
.WillOnce(Return(R"({"status": "ok"})"));
auto result = service.fetchData();
EXPECT_EQ(result.status, "ok");
}
패턴 3: 파일 시스템 Mock
class FileSystem {
public:
virtual ~FileSystem() = default;
virtual bool exists(const std::string& path) = 0;
virtual std::string read(const std::string& path) = 0;
virtual bool write(const std::string& path, const std::string& content) = 0;
};
class MockFileSystem : public FileSystem {
public:
MOCK_METHOD(bool, exists, (const std::string&), (override));
MOCK_METHOD(std::string, read, (const std::string&), (override));
MOCK_METHOD(bool, write, (const std::string&, const std::string&), (override));
};
TEST(ConfigLoaderTest, LoadConfig) {
MockFileSystem mockFs;
ConfigLoader loader(&mockFs);
EXPECT_CALL(mockFs, exists("/etc/config.json"))
.WillOnce(Return(true));
EXPECT_CALL(mockFs, read("/etc/config.json"))
.WillOnce(Return(R"({"port": 8080})"));
auto config = loader.load();
EXPECT_EQ(config.port, 8080);
}
패턴 4: 타이머 Mock
Cache가 만료 시간을 판단할 때 Clock::now()를 쓰도록 하면, 테스트에서 MockClock으로 “지금은 1000”, “30초 후 1030”, “70초 후 1070”처럼 시간을 고정할 수 있습니다. 실제로 70초 기다리지 않고도 “TTL 60초가 지나면 만료된다”는 동작을 검증할 수 있어, 시간에 의존하는 로직을 빠르고 안정적으로 테스트할 수 있습니다.
class Clock {
public:
virtual ~Clock() = default;
virtual time_t now() = 0;
};
class MockClock : public Clock {
public:
MOCK_METHOD(time_t, now, (), (override));
};
TEST(CacheTest, Expiration) {
MockClock mockClock;
Cache cache(&mockClock);
// 현재 시간: 1000
EXPECT_CALL(mockClock, now())
.WillOnce(Return(1000));
cache.set("key", "value", 60); // 60초 TTL
// 30초 후
EXPECT_CALL(mockClock, now())
.WillOnce(Return(1030));
EXPECT_TRUE(cache.has("key")); // 아직 유효
// 70초 후
EXPECT_CALL(mockClock, now())
.WillOnce(Return(1070));
EXPECT_FALSE(cache.has("key")); // 만료됨
}
패턴 5: StrictMock vs NiceMock
StrictMock은 EXPECT_CALL로 등록되지 않은 메서드가 호출되면 즉시 실패합니다. “의도하지 않은 호출”을 엄격히 막을 때 유용합니다. NiceMock은 등록되지 않은 호출을 경고만 하고 무시합니다. Mock이 많은 메서드를 가질 때 “지금 테스트에 필요한 것만 검증”하고 싶을 때 쓰면 편합니다.
// StrictMock: 등록 안 된 호출 시 테스트 실패
TEST(StrictTest, UnregisteredCallFails) {
::testing::StrictMock<MockDatabase> mockDb;
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(nullptr));
mockDb.findUser("alice"); // OK
// mockDb.saveUser(...); // 호출하면 실패
}
// NiceMock: 등록 안 된 호출은 무시 (경고만)
TEST(NiceTest, UnregisteredCallIgnored) {
::testing::NiceMock<MockDatabase> mockDb;
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(nullptr));
mockDb.findUser("alice"); // OK
// mockDb.saveUser(...); // 경고만, 테스트 통과
}
6. 자주 발생하는 에러와 해결법
에러 1: “Actual function call count doesn’t match”
증상:
Actual function call count doesn't match EXPECT_CALL(mockDb, findUser("alice"))...
Expected: to be called once
Actual: never called
원인: EXPECT_CALL로 기대한 메서드가 한 번도 호출되지 않았다. 테스트 대상 코드가 해당 메서드를 호출하지 않거나, 인자가 달라서 매치되지 않았다.
해결:
- 테스트 대상 코드가 실제로
findUser를 호출하는지 확인한다. - 인자 매처를
_로 완화해 “어떤 인자든” 매치되게 한 뒤, 실제 전달되는 인자를 확인한다. EXPECT_CALL이 테스트 대상 코드 실행 전에 등록되어 있는지 확인한다.
// ❌ 잘못된 예: service.login() 호출 전에 EXPECT_CALL이 없음
TEST(Bad, OrderWrong) {
MockDatabase mockDb;
UserService service(&mockDb);
service.login("alice", "pass"); // 먼저 실행됨
EXPECT_CALL(mockDb, findUser("alice")); // 너무 늦음
}
// ✅ 올바른 예: EXPECT_CALL 먼저, 그 다음 실행
TEST(Good, OrderCorrect) {
MockDatabase mockDb;
UserService service(&mockDb);
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(nullptr));
service.login("alice", "pass");
}
에러 2: “Uninteresting mock function call”
증상:
Uninteresting mock function call - returning default value.
Function call: findUser("bob")
원인: EXPECT_CALL로 등록하지 않은 메서드가 호출되었다. StrictMock을 쓰면 테스트가 실패하고, 기본 Mock은 경고만 낸다.
해결:
- 해당 호출에 대한
EXPECT_CALL을 추가한다. - 또는
NiceMock을 사용해 “지금 검증하지 않는 호출”은 무시한다. - “이 메서드는 호출되면 안 된다”는 의도라면
EXPECT_CALL(mock, method(_)).Times(0)으로 명시한다.
// ✅ 해결: bob에 대한 EXPECT_CALL 추가
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
EXPECT_CALL(mockDb, findUser("bob")).WillRepeatedly(Return(nullptr));
에러 3: “MOCK_METHOD must be used in a class”
증상: MOCK_METHOD를 클래스 밖이나 일반 함수 안에서 사용하면 컴파일 에러가 난다.
원인: MOCK_METHOD는 클래스 정의 내부에서만 사용할 수 있는 매크로다.
해결:
- Mock 클래스를 정의하고, 그 클래스 내부에
MOCK_METHOD를 넣는다.
// ❌ 잘못된 예
MOCK_METHOD(User*, findUser, (const std::string&), (override)); // 클래스 밖
// ✅ 올바른 예
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
};
에러 4: “reference to non-static member function must be called”
증상: Invoke에 멤버 함수를 넘길 때 “reference to non-static member function” 에러가 난다.
원인: 비정적 멤버 함수는 this가 필요하므로, 그대로 함수 포인터로 넘기면 안 된다.
해결:
- 람다로 감싸서
Invoke에 넘긴다. - 또는
std::bind를 사용한다.
// ❌ 잘못된 예
EXPECT_CALL(mockDb, findUser(_)).WillRepeatedly(Invoke(&helper::lookup));
// ✅ 올바른 예
EXPECT_CALL(mockDb, findUser(_)).WillRepeatedly(Invoke([this](const std::string& s) {
return helper.lookup(s);
}));
에러 5: Mock 객체 수명 문제 (dangling pointer)
증상: 테스트가 “가끔” 크래시하거나, AddressSanitizer에서 use-after-free를 보고한다.
원인: WillOnce(Return(&localUser))처럼 로컬 변수의 주소를 반환했는데, 테스트가 끝난 뒤 그 주소를 사용하는 코드가 있다. 또는 Mock 객체가 테스트보다 먼저 소멸되었다.
해결:
- 반환하는 객체가 테스트 전체 수명 동안 유효한지 확인한다.
- 가능하면
std::shared_ptr등 스마트 포인터를 사용하거나, 테스트 픽스처에 객체를 두어 수명을 늘린다.
// ❌ 위험: localUser는 함수 끝나면 소멸
TEST(Bad, DanglingPointer) {
MockDatabase mockDb;
User localUser{"alice", "pass"};
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&localUser));
service.login("alice", "pass"); // service가 나중에 localUser를 참조하면 UB
}
// ✅ 안전: fixture나 멤버로 수명 확보
class UserServiceTest : public ::testing::Test {
protected:
User alice{"alice", "pass123"};
};
TEST_F(UserServiceTest, LoginSuccess) {
MockDatabase mockDb;
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
// alice는 테스트 끝날 때까지 유효
}
에러 6: “Overloaded function” 컴파일 에러
증상: MOCK_METHOD에서 오버로드된 함수를 선언할 때 컴파일 에러가 난다.
원인: MOCK_METHOD는 함수 시그니처를 괄호로 묶어야 하는데, 오버로드 시 구문이 헷갈린다.
해결:
(반환타입, 메서드이름, (인자들), (override))형식을 정확히 따른다.- 오버로드된 메서드는 각각 별도의
MOCK_METHOD로 선언한다.
// ✅ 오버로드된 메서드
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
MOCK_METHOD(User*, findUser, (int), (override)); // 다른 시그니처
};
7. 테스트 전략
전략 1: 행위 검증 vs 상태 검증
상태 검증: 메서드 호출 결과(반환값, 객체 상태)만 검사한다. “login이 true를 반환하는가?”
행위 검증: 메서드가 어떻게 호출되었는지 검사한다. “findUser가 정확히 한 번, ‘alice’ 인자로 호출되었는가?”
GMock은 행위 검증에 특화되어 있다. EXPECT_CALL로 “이 메서드가 이 인자로 몇 번 호출되는가”를 검증한다. 상태 검증은 EXPECT_EQ, EXPECT_TRUE 등 GTest assertion으로 한다. 둘을 함께 쓰면 “호출이 올바르게 됐는지”와 “최종 결과가 맞는지”를 모두 확인할 수 있다.
TEST(UserServiceTest, BothVerifications) {
MockDatabase mockDb;
UserService service(&mockDb);
User alice{"alice", "pass123"};
// 행위 검증: findUser가 "alice"로 1번 호출
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
// 상태 검증: login 결과가 true
EXPECT_TRUE(service.login("alice", "pass123"));
}
전략 2: 테스트 격리
각 테스트는 다른 테스트에 영향받지 않아야 한다. Mock 객체는 테스트마다 새로 만들고, EXPECT_CALL도 테스트 내에서만 유효하다. 공통 설정이 필요하면 테스트 픽스처를 사용한다.
class UserServiceTest : public ::testing::Test {
protected:
void SetUp() override {
mockDb = std::make_unique<MockDatabase>();
service = std::make_unique<UserService>(mockDb.get());
}
std::unique_ptr<MockDatabase> mockDb;
std::unique_ptr<UserService> service;
User alice{"alice", "pass123"};
};
TEST_F(UserServiceTest, LoginSuccess) {
EXPECT_CALL(*mockDb, findUser("alice")).WillOnce(Return(&alice));
EXPECT_TRUE(service->login("alice", "pass123"));
}
TEST_F(UserServiceTest, LoginFailure) {
EXPECT_CALL(*mockDb, findUser("alice")).WillOnce(Return(nullptr));
EXPECT_FALSE(service->login("alice", "wrong"));
}
전략 3: Given-When-Then 구조
테스트를 Given(준비) - When(실행) - Then(검증) 구조로 쓰면 가독성이 좋아진다. Mock 설정은 Given, 테스트 대상 호출은 When, EXPECT_*는 Then에 해당한다.
TEST(UserServiceTest, LoginSuccess_GivenUserExists_WhenLogin_ThenReturnsTrue) {
// Given: alice 사용자가 DB에 존재
MockDatabase mockDb;
UserService service(&mockDb);
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
// When: 올바른 비밀번호로 로그인 시도
bool result = service.login("alice", "pass123");
// Then: 성공
EXPECT_TRUE(result);
}
전략 4: 엣지 케이스 우선
Mock을 쓰면 nullptr 반환, 예외, 빈 문자열 같은 엣지 케이스를 쉽게 시뮬레이션할 수 있다. 이런 경우를 테스트해 두면 프로덕션에서의 버그를 줄일 수 있다.
TEST(UserServiceTest, Login_WhenUserNotFound_ReturnsFalse) {
MockDatabase mockDb;
UserService service(&mockDb);
EXPECT_CALL(mockDb, findUser("unknown")).WillOnce(Return(nullptr));
EXPECT_FALSE(service.login("unknown", "any"));
}
TEST(UserServiceTest, Login_WhenDbThrows_PropagatesException) {
MockDatabase mockDb;
UserService service(&mockDb);
EXPECT_CALL(mockDb, findUser("alice"))
.WillOnce(Throw(std::runtime_error("DB error")));
EXPECT_THROW(service.login("alice", "pass"), std::runtime_error);
}
8. 프로덕션 패턴
패턴 1: 인터페이스와 구현 분리
프로덕션 코드에서도 인터페이스(추상 클래스)와 구현(RealDatabase, MockDatabase)을 분리해 두면, 테스트뿐 아니라 구현 교체(예: SQLite → PostgreSQL)가 쉬워진다. 헤더에는 인터페이스만 노출하고, 구현은 내부에 둔다.
// database_interface.h - 외부에 노출
class Database {
public:
virtual ~Database() = default;
virtual User* findUser(const std::string& username) = 0;
virtual bool saveUser(const User& user) = 0;
};
// real_database.h / mock_database.h - 구현
class RealDatabase : public Database { /* ... */ };
class MockDatabase : public Database { /* ... */ }; // 테스트 전용
패턴 2: 팩토리로 의존성 주입
생성자에서 직접 Database*를 받지 않고, 팩토리를 주입해 “테스트 시에는 Mock 팩토리, 프로덕션에서는 Real 팩토리”를 쓰게 할 수 있다. 대규모 프로젝트에서 의존성 그래프를 단순하게 유지할 때 유용하다.
class DatabaseFactory {
public:
virtual ~DatabaseFactory() = default;
virtual std::unique_ptr<Database> create() = 0;
};
class MockDatabaseFactory : public DatabaseFactory {
public:
MOCK_METHOD(std::unique_ptr<Database>, create, (), (override));
};
// UserService가 factory->create()로 DB를 얻도록 설계
패턴 3: Mock 헤더 분리
Mock 클래스는 테스트 전용이므로, 프로덕션 빌드에 포함하지 않는다. #ifdef TESTING 또는 별도 헤더(mock_database.h)로 분리하고, 테스트 타겟에서만 링크한다. 프로덕션 바이너리 크기와 컴파일 시간을 줄일 수 있다.
# CMakeLists.txt
add_library(mocks mock_database.cpp)
target_include_directories(mocks PRIVATE ${CMAKE_SOURCE_DIR}/test/mocks)
add_executable(myapp_test test_main.cpp)
target_link_libraries(myapp_test PRIVATE gtest_main gmock mocks)
# 프로덕션 myapp에는 mocks 링크하지 않음
패턴 4: 재사용 가능한 Mock 픽스처
여러 테스트에서 공통으로 쓰는 Mock 설정을 헬퍼 함수나 픽스처 메서드로 추출한다. 테스트 코드 중복을 줄이고, Mock 설정 변경 시 한 곳만 수정하면 된다.
class UserServiceTest : public ::testing::Test {
protected:
void ExpectFindUser(const std::string& name, User* user) {
EXPECT_CALL(*mockDb, findUser(name)).WillOnce(Return(user));
}
void ExpectFindUserNotFound(const std::string& name) {
EXPECT_CALL(*mockDb, findUser(name)).WillOnce(Return(nullptr));
}
std::unique_ptr<MockDatabase> mockDb;
User alice{"alice", "pass123"};
};
TEST_F(UserServiceTest, LoginSuccess) {
ExpectFindUser("alice", &alice);
EXPECT_TRUE(service->login("alice", "pass123"));
}
완전한 실행 예제: CMake + GMock
아래는 복사해 붙여넣어 바로 빌드·실행할 수 있는 최소 예제다.
CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(gmock_example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
add_executable(user_service_test test_user_service.cpp)
target_link_libraries(user_service_test PRIVATE GTest::gtest_main gmock)
enable_testing()
add_test(NAME user_service_test COMMAND user_service_test)
test_user_service.cpp:
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
struct User {
std::string name;
std::string password;
};
class Database {
public:
virtual ~Database() = default;
virtual User* findUser(const std::string& username) = 0;
};
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
};
class UserService {
Database* db;
public:
explicit UserService(Database* database) : db(database) {}
bool login(const std::string& username, const std::string& password) {
auto* user = db->findUser(username);
return user && user->password == password;
}
};
TEST(UserServiceTest, LoginSuccess) {
MockDatabase mockDb;
UserService service(&mockDb);
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
EXPECT_TRUE(service.login("alice", "pass123"));
}
TEST(UserServiceTest, LoginFailure_UserNotFound) {
MockDatabase mockDb;
UserService service(&mockDb);
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(nullptr));
EXPECT_FALSE(service.login("alice", "pass123"));
}
TEST(UserServiceTest, LoginFailure_WrongPassword) {
MockDatabase mockDb;
UserService service(&mockDb);
User alice{"alice", "pass123"};
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
EXPECT_FALSE(service.login("alice", "wrong"));
}
실행:
mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
- C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
이 글에서 다루는 키워드 (관련 검색어)
C++ Google Mock, gmock, Mock 객체, 의존성 주입, 테스트 더블, 단위 테스트 모킹 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 기능 | 사용법 |
|---|---|
| Mock 생성 | MOCK_METHOD |
| 호출 예상 | EXPECT_CALL |
| 반환값 | WillOnce(Return(...)) |
| 예외 | WillOnce(Throw(...)) |
| 인자 매칭 | _, Eq, StartsWith |
| 호출 횟수 | Times(n) |
| 호출 순서 | InSequence |
| 엄격/관대 | StrictMock, NiceMock |
핵심 원칙:
- 인터페이스로 의존성 분리
- Mock으로 외부 의존성 제거
- EXPECT_CALL로 동작 검증
- 의존성 주입 패턴 사용
- 테스트는 빠르고 독립적으로
구현 체크리스트
- 인터페이스(추상 클래스) 정의
- Mock 클래스에
MOCK_METHOD선언 - 테스트에서
EXPECT_CALL을 실행 전에 등록 - 반환 객체 수명 확인 (dangling pointer 방지)
- 엣지 케이스 테스트 (nullptr, 예외)
- Mock 헤더를 프로덕션 빌드에서 제외
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Google Mock으로 Mock 객체를 만들고, 의존성을 분리하며, 실전에서 테스트 가능한 설계를 하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 Google Mock 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: Google Mock으로 인터페이스를 모킹해 단위 테스트를 격리할 수 있습니다. 다음으로 생성 패턴(#19-1)를 읽어보면 좋습니다.
이전 글: [C++ 실전 가이드 #18-1] Google Test로 단위 테스트 작성하기
다음 글: [C++ 실전 가이드 #19-1] 생성 패턴: Singleton, Factory, Builder
관련 글
- C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
- C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열
- C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)