C++ Google Mock | "DB 없이 테스트하고 싶어요" Mock 객체로 의존성 분리

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_METHODfindUser의 호출을 가로챕니다. 테스트에서는 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 기초
  2. EXPECT_CALL
  3. 반환값 설정
  4. 인자 매칭
  5. 실전 패턴
  6. 자주 발생하는 에러와 해결법
  7. 테스트 전략
  8. 프로덕션 패턴

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: 의존성 주입

UserServiceDatabase*를 생성자 인자로 받으면, 테스트에서는 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

핵심 원칙:

  1. 인터페이스로 의존성 분리
  2. Mock으로 외부 의존성 제거
  3. EXPECT_CALL로 동작 검증
  4. 의존성 주입 패턴 사용
  5. 테스트는 빠르고 독립적으로

구현 체크리스트

  • 인터페이스(추상 클래스) 정의
  • Mock 클래스에 MOCK_METHOD 선언
  • 테스트에서 EXPECT_CALL을 실행 전에 등록
  • 반환 객체 수명 확인 (dangling pointer 방지)
  • 엣지 케이스 테스트 (nullptr, 예외)
  • Mock 헤더를 프로덕션 빌드에서 제외

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Google Mock으로 Mock 객체를 만들고, 의존성을 분리하며, 실전에서 테스트 가능한 설계를 하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreferenceGoogle 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 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)