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

같이 보면 좋은 글


Google Mock으로 인터페이스를 모킹해 단위 테스트를 격리할 수 있습니다. 다음으로 생성 패턴(#19-1)를 읽어보면 좋습니다.

이전 글: [C++ 실전 가이드 #18-1] Google Test로 단위 테스트 작성하기

다음 글: [C++ 실전 가이드 #19-1] 생성 패턴: Singleton, Factory, Builder