Google Mock으로 C++ 의존성 분리하기: MOCK_METHOD·EXPECT_CALL·매처
들어가며: “데이터베이스 없이 테스트하고 싶어요”
데이터베이스를 사용하는 코드를 테스트하려면 테스트마다 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를 반환하는지”만 검증할 수 있습니다. 이 예제들은 아래처럼 GMock의 이름을 가져왔다고 가정합니다.
#include <gmock/gmock.h>
using ::testing::_;
using ::testing::Return;
using ::testing::Throw;
using ::testing::Invoke;
using ::testing::AtLeast;
using ::testing::AtMost;
using ::testing::InSequence;
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"));
}
인터페이스 정의와 MOCK_METHOD로 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");
}
EXPECT_CALL로 호출 횟수와 순서 검증
호출 횟수
Times(n)은 정확히 n번, Times(AtLeast(2))는 2번 이상, Times(AtMost(3))는 최대 3번, Times(0)은 호출되면 안 됨을 의미합니다. “이 메서드는 한 번만 불려야 한다”거나 “반복문 안에서 여러 번 불릴 수 있다” 같은 시나리오를 명시적으로 검증할 수 있습니다.
TEST(DatabaseTest, CallCounts) {
MockDatabase mockDb;
EXPECT_CALL(mockDb, findUser("alice")).Times(1); // 정확히 1번
EXPECT_CALL(mockDb, findUser("bob")).Times(AtLeast(2)); // 2번 이상
EXPECT_CALL(mockDb, findUser("dave")).Times(0); // 호출 금지
mockDb.findUser("alice");
mockDb.findUser("bob");
mockDb.findUser("bob");
}
기대는 Mock 객체가 소멸할 때 검사되므로, 여기서 bob을 한 번만 호출하면 테스트 끝에서 실패합니다. 이 글의 나머지 짧은 예제들은 기대 설정 부분만 보여 주는 경우가 있는데, 실제 테스트에서는 등록한 기대가 모두 충족되도록 테스트 대상 코드를 실행해야 합니다.
호출 순서
InSequence seq;를 선언한 뒤 나오는 EXPECT_CALL들은 선언된 순서대로 호출되어야 합니다. findUser → saveUser → deleteUser 순서가 아니면 테스트가 실패하므로, “먼저 찾고, 저장하고, 삭제한다”처럼 순서 자체가 요구사항일 때 씁니다. _는 임의의 인자를 의미하는 매처입니다. 순서가 중요하지 않은 호출까지 InSequence로 묶으면 구현을 조금만 바꿔도 테스트가 깨지므로, 꼭 필요한 호출에만 거는 편이 좋습니다.
TEST(DatabaseTest, CallOrder) {
MockDatabase mockDb;
InSequence seq; // 이후 기대는 이 순서대로 충족되어야 함
EXPECT_CALL(mockDb, findUser("alice"));
EXPECT_CALL(mockDb, saveUser(_));
EXPECT_CALL(mockDb, deleteUser(_));
// ... 테스트 대상 코드가 이 순서로 호출해야 통과
}
WillOnce, WillRepeatedly, Invoke로 동작 지정
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);
}
기본·문자열·커스텀 매처로 인자 검사
기본 매처
_는 “어떤 인자든 허용”하는 매처입니다. Eq(“alice”)는 값이 “alice”와 같을 때만 매치하고, Ne(0), Gt(100)은 “0이 아님”, “100 초과”를 검사합니다. 인자 값이 테스트 시점에 고정이 아니거나, “0이 아닌 ID”처럼 조건만 중요할 때 매처를 쓰면 EXPECT_CALL을 유연하게 작성할 수 있습니다.
// 필요한 모듈 import
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")));
mockDb.findUser("admin_root");
mockDb.findUser("[email protected]");
}
한 호출이 여러 기대와 동시에 매치되면 GMock은 나중에 선언된 기대부터 확인합니다. 예를 들어 "[email protected]"은 두 매처에 모두 맞으므로 아래쪽 EndsWith 기대가 소비합니다. 기대가 겹치지 않게 쓰거나, 겹친다면 구체적인 기대를 뒤에 선언합니다.
커스텀 매처
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")));
}
네트워크, 파일 시스템, 타이머 Mock과 StrictMock
패턴 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
기본 Mock(naggy mock)은 EXPECT_CALL로 등록되지 않은 메서드가 호출되면 “Uninteresting mock function call” 경고를 출력하고 기본값을 반환합니다. StrictMock으로 감싸면 이런 호출이 곧바로 테스트 실패가 되어 의도하지 않은 호출을 엄격히 막을 수 있고, NiceMock으로 감싸면 경고 없이 조용히 기본값을 반환합니다. 메서드가 많은 Mock에서 “지금 테스트에 필요한 것만 검증”하고 싶을 때 NiceMock이 편합니다.
// 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(...); // 조용히 false 반환, 테스트 통과
}
호출 횟수 불일치, Uninteresting call, Mock 수명 문제
에러 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 사용
MOCK_METHOD는 멤버 함수 선언으로 펼쳐지는 매크로라 클래스 정의 안에서만 쓸 수 있습니다. 클래스 밖에 쓰면 매크로가 펼쳐진 코드에서 알아보기 어려운 컴파일 에러가 납니다.
// ❌ 클래스 밖
MOCK_METHOD(User*, findUser, (const std::string&), (override));
// ✅ Mock 클래스 안
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
};
에러 4: Invoke에 멤버 함수를 그대로 넘김
비정적 멤버 함수는 호출할 객체가 필요하므로 Invoke(&Helper::lookup)처럼 멤버 함수 포인터만 넘기면 컴파일되지 않습니다. 객체 포인터를 함께 넘기는 Invoke(&obj, &Class::method) 형태를 쓰거나 람다로 감쌉니다.
struct Helper {
User* lookup(const std::string& name);
};
Helper helper;
// ❌ 객체 없이 멤버 함수 포인터만
// EXPECT_CALL(mockDb, findUser(_)).WillRepeatedly(Invoke(&Helper::lookup));
// ✅ 객체 포인터 + 멤버 함수 포인터
EXPECT_CALL(mockDb, findUser(_)).WillRepeatedly(Invoke(&helper, &Helper::lookup));
// ✅ 람다로 감싸기
EXPECT_CALL(mockDb, findUser(_)).WillRepeatedly(Invoke([&helper](const std::string& s) {
return helper.lookup(s);
}));
최신 GoogleTest에서는 Invoke 없이 WillRepeatedly([&](const std::string& s) { ... })처럼 호출 가능한 객체를 바로 넘겨도 됩니다.
에러 5: Mock 객체 수명 문제 (dangling pointer)
테스트가 “가끔” 크래시하거나 AddressSanitizer가 use-after-free를 보고한다면 수명 문제를 의심합니다. WillOnce(Return(&localUser))의 localUser는 테스트 함수가 끝날 때 소멸하므로, 테스트 대상이 그 포인터를 테스트보다 오래 사는 곳(전역 캐시, static 변수, 다른 스레드)에 저장해 두었다면 테스트가 끝난 뒤 해제된 객체를 가리키게 됩니다. 또 Mock 객체가 그것을 쓰는 서비스보다 먼저 소멸하면, 서비스 소멸자나 백그라운드 작업이 이미 사라진 Mock을 호출합니다. 지역 변수는 선언의 역순으로 소멸하므로 Mock을 서비스보다 먼저 선언해야 합니다.
// ❌ 서비스가 Mock보다 먼저 선언되어 Mock이 먼저 소멸
TEST(Bad, DestructionOrder) {
UserService* service = nullptr;
MockDatabase mockDb;
// ...
}
// ✅ 반환 객체를 픽스처 멤버로 두어 테스트 전체 수명 동안 유효하게
class UserServiceFixture : public ::testing::Test {
protected:
MockDatabase mockDb; // 먼저 선언 → 나중에 소멸
UserService service{&mockDb};
User alice{"alice", "pass123"};
};
TEST_F(UserServiceFixture, LoginSuccess) {
EXPECT_CALL(mockDb, findUser("alice")).WillOnce(Return(&alice));
EXPECT_TRUE(service.login("alice", "pass123"));
}
에러 6: 오버로드된 메서드 모킹
MOCK_METHOD는 (반환타입, 메서드이름, (인자들), (override)) 형식을 따라야 하고, 오버로드된 메서드는 시그니처마다 별도의 MOCK_METHOD로 선언합니다. (override)를 붙였다면 인터페이스에도 같은 시그니처의 가상 함수가 있어야 합니다. 인자 타입에 쉼표가 들어가는 템플릿(std::map<int, int> 등)은 괄호로 한 번 더 감쌉니다.
class Database {
public:
virtual ~Database() = default;
virtual User* findUser(const std::string& username) = 0;
virtual User* findUser(int id) = 0; // 오버로드
};
class MockDatabase : public Database {
public:
MOCK_METHOD(User*, findUser, (const std::string&), (override));
MOCK_METHOD(User*, findUser, (int), (override));
MOCK_METHOD((std::map<int, int>), stats, (), ()); // 쉼표가 있는 반환 타입
};
// _ 하나로는 어느 오버로드인지 모호하므로 타입을 지정
EXPECT_CALL(mockDb, findUser(::testing::An<int>()));
EXPECT_CALL(mockDb, findUser(::testing::Matcher<const std::string&>(_)));
행위 검증과 상태 검증, 테스트 격리
전략 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);
}
인터페이스 분리, 팩토리 주입, Mock 헤더 분리
패턴 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 팩토리”를 쓰게 할 수 있습니다. 서비스가 DB 연결을 필요한 시점에 여러 번 만들어야 하는 경우에 유용합니다.
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));
};
// unique_ptr처럼 복사할 수 없는 값은 ByMove로 반환
auto db = std::make_unique<MockDatabase>();
EXPECT_CALL(factory, create()).WillOnce(Return(::testing::ByMove(std::move(db))));
패턴 3: Mock 헤더 분리
Mock 클래스는 테스트 전용이므로 프로덕션 빌드에 포함하지 않습니다. test/mocks/mock_database.h 같은 별도 디렉터리에 두고 테스트 타깃에서만 사용합니다.
# CMakeLists.txt
add_library(mocks INTERFACE)
target_include_directories(mocks INTERFACE ${CMAKE_SOURCE_DIR}/test/mocks)
target_link_libraries(mocks INTERFACE GTest::gmock)
add_executable(myapp_test test_main.cpp)
target_link_libraries(myapp_test PRIVATE GTest::gtest_main mocks)
# 프로덕션 myapp에는 mocks를 링크하지 않음
MOCK_METHOD만 있는 Mock 클래스는 헤더만으로 충분하므로 INTERFACE 라이브러리로 두면 됩니다.
패턴 4: 재사용 가능한 Mock 픽스처
여러 테스트에서 공통으로 쓰는 Mock 설정을 픽스처의 헬퍼 메서드로 추출하면 중복이 줄고, 설정을 바꿀 때 한 곳만 수정하면 됩니다.
class UserServiceTest : public ::testing::Test {
protected:
void ExpectFindUser(const std::string& name, User* user) {
EXPECT_CALL(mockDb, findUser(name)).WillOnce(Return(user));
}
MockDatabase mockDb;
UserService service{&mockDb};
User alice{"alice", "pass123"};
};
TEST_F(UserServiceTest, LoginSuccess) {
ExpectFindUser("alice", &alice);
EXPECT_TRUE(service.login("alice", "pass123"));
}
TEST_F(UserServiceTest, LoginFailsWhenUserMissing) {
ExpectFindUser("alice", nullptr);
EXPECT_FALSE(service.login("alice", "pass123"));
}
완전한 실행 예제: CMake + GMock
아래는 복사해 붙여넣어 바로 빌드·실행할 수 있는 최소 예제입니다. using ::testing::Return;을 빠뜨리면 'Return' was not declared in this scope 에러가 나는데, 예제를 조각으로 옮길 때 가장 흔히 놓치는 부분입니다.
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 GTest::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>
using ::testing::Return;
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 | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
- 스마트 포인터 입문
- unique_ptr 고급 활용
- shared_ptr 심화
Google Mock으로 인터페이스를 모킹해 단위 테스트를 격리할 수 있습니다. 다음으로 생성 패턴(#19-1)를 읽어보면 좋습니다.