본문으로 건너뛰기 C++ Google Mock | 'DB 없이 테스트하고 싶어요' Mock 객체로 의존성 분리

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

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

💡 초보자를 위한 한 줄: DB·API 같은 외부 의존을 인터페이스로 추상화하고 MOCK_METHOD로 가짜 구현을 만들면, 실제 환경 없이 빠르게 단위 테스트할 수 있습니다.

들어가며: “데이터베이스 없이 테스트하고 싶어요”

외부 의존성 때문에 테스트가 어렵다

데이터베이스를 사용하는 코드를 테스트하고 싶었습니다. 하지만:

  • 테스트마다 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 기초

인터페이스 정의

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번 호출됐는지” 검사합니다. 호출 횟수가 맞지 않거나 아예 호출되지 않으면 테스트가 실패합니다.

C/C++ 예제 코드입니다.

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)은 호출되면 안 됨을 의미합니다. “이 메서드는 한 번만 불려야 한다”거나 “반복문 안에서 여러 번 불릴 수 있다” 같은 시나리오를 명시적으로 검증할 수 있어, 로직이 예상대로 호출하는지 확인하는 데 유용합니다.

C/C++ 예제 코드입니다.

// 실행 예제
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 순서가 아니면 테스트가 실패합니다. “먼저 사용자를 찾으며, 수정한 뒤, 저장하며, 삭제한다” 같은 시나리오에서 순서가 바뀌면 버그인 경우에 유용합니다. _임의의 인자를 의미하는 매처입니다.

C/C++ 예제 코드입니다.

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를 여러 개 나열하면 첫 번째 호출·두 번째 호출… 순서대로 다른 값을 반환하게 할 수 있습니다.

C/C++ 예제 코드입니다.

// 실행 예제
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 반환
}

여러 반환값

C/C++ 예제 코드입니다.

// 실행 예제
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)를 넣거나, 인자를 기반으로 동적 반환값을 만들 때 유용합니다.

C/C++ 예제 코드입니다.

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을 유연하게 작성할 수 있습니다.

// 필요한 모듈 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")));
    EXPECT_CALL(mockDb, findUser(HasSubstr("test")));
}

커스텀 매처

MATCHER_P(이름, 파라미터, 설명)로 사용자 정의 매처를 만듭니다. arg는 EXPECT_CALL에 넘긴 인자(여기서는 User 객체)이으며, return arg.name == name으로 “name 필드가 파라미터와 같은지” 검사합니다. saveUser(IsUserWithName(“alice”))는 “saveUser에 넘기는 User의 name이 alice일 때만” 이 호출에 매치됩니다. 복잡한 구조체나 비즈니스 조건을 한 번 정의해 두고 여러 테스트에서 재사용할 수 있습니다.

C/C++ 예제 코드입니다.

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이 많은 메서드를 가질 때 “지금 테스트에 필요한 것만 검증”하고 싶을 때 쓰면 편합니다.

C/C++ 예제 코드입니다.

// 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이 테스트 대상 코드 실행 전에 등록되어 있는지 확인한다.

C/C++ 예제 코드입니다.

// ❌ 잘못된 예: 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를 사용한다.

C/C++ 예제 코드입니다.

// ❌ 잘못된 예
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으로 한다. 둘을 함께 쓰면 “호출이 올바르게 됐는지”와 “최종 결과가 맞는지”를 모두 확인할 수 있다.

C/C++ 예제 코드입니다.

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에 해당한다.

C/C++ 예제 코드입니다.

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 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_METHOD로 Mock 클래스를 만들었는가?
  • EXPECT_CALL로 “이 메서드가 이렇게 호출될 것”을 검증했는가?

💡 Tip: Mock 기초를 다시 보려면 #mock-basics 섹션으로 이동하세요.


구현 체크리스트

  • 인터페이스(추상 클래스) 정의
  • 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


관련 글