假设我们要测试一个 UserService类,它提供了一个 RegisterUser方法。该方法的核心逻辑是验证输入参数,然后通过一个 IDatabase接口将用户信息保存到数据库。为了在不连接真实数据库的情况下测试 UserService的逻辑,我们将使用 GMock 创建一个 MockDatabase
gtest_gmock_demo/
├── CMakeLists.txt
├── include/
│ ├── database.h
│ └── user_service.h
├── src/
│ └── user_service.cpp
└── tests/
└── user_service_test.cpp
1. 定义依赖接口
(include/database.h)
首先,定义一个抽象的数据库接口。这是依赖注入的关键,也是创建Mock类的基础
// include/database.h
#pragma once
#include <string>
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual bool Connect(const std::string& db_uri) = 0;
virtual bool SaveUser(const std::string& username, int age) = 0;
};
2. 实现业务逻辑
(include/user_service.h, src/user_service.cpp)
接下来,实现被测试的业务类 UserService。它在构造函数中接收一个 IDatabase指针,这使我们能够在测试时轻松地注入Mock对象
// include/user_service.h
#pragma once
#include "database.h"
#include <string>
class UserService {
public:
// 通过构造函数注入数据库依赖
UserService(IDatabase* database);
bool RegisterUser(const std::string& username, int age);
private:
IDatabase* database_;
};
// src/user_service.cpp
#include "user_service.h"
UserService::UserService(IDatabase* database) : database_(database) {}
bool UserService::RegisterUser(const std::string& username, int age) {
// 1. 参数校验逻辑(这是我们测试的重点之一)
if (database_ == nullptr || username.empty() || age <= 0) {
return false;
}
// 2. 通过依赖接口保存数据
return database_->SaveUser(username, age);
}
3. 创建Mock类
(tests/user_service_test.cpp)
使用 GMock 的 MOCK_METHOD宏为 IDatabase接口创建Mock类
// tests/user_service_test.cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "../include/user_service.h"
#include "../include/database.h"
// 创建 Mock 类
class MockDatabase : public IDatabase {
public:
MOCK_METHOD(bool, Connect, (const std::string& db_uri), (override));
MOCK_METHOD(bool, SaveUser, (const std::string& username, int age), (override));
};
4. 编写测试用例
(tests/user_service_test.cpp)
针对不同的业务场景编写测试用例,使用 EXPECT_CALL来设定Mock对象的预期行为
// 继续在 tests/user_service_test.cpp 中编写
// 测试用例1:用户注册成功
TEST(UserServiceTest, RegisterUser_Success) {
// 1. 创建Mock对象
MockDatabase mock_db;
UserService user_service(&mock_db);
// 2. 设定期望:SaveUser方法应被调用一次,参数为"Alice"和30,并返回true
EXPECT_CALL(mock_db, SaveUser("Alice", 30))
.Times(1)
.WillOnce(testing::Return(true));
// 3. 执行被测试方法,并断言结果
EXPECT_TRUE(user_service.RegisterUser("Alice", 30));
}
// 测试用例2:用户名为空,注册失败,且不应调用数据库保存
TEST(UserServiceTest, RegisterUser_Fail_EmptyUsername) {
MockDatabase mock_db;
UserService user_service(&mock_db);
// 设定期望:SaveUser方法不应被调用任何次数
EXPECT_CALL(mock_db, SaveUser(testing::_, testing::_)).Times(0);
EXPECT_FALSE(user_service.RegisterUser("", 25)); // 用户名为空
}
// 测试用例3:年龄非法,注册失败
TEST(UserServiceTest, RegisterUser_Fail_InvalidAge) {
MockDatabase mock_db;
UserService user_service(&mock_db);
EXPECT_CALL(mock_db, SaveUser(testing::_, testing::_)).Times(0);
EXPECT_FALSE(user_service.RegisterUser("Bob", -5)); // 年龄为负数
}
5. 配置CMake项目
使用 CMake 来管理项目构建和测试依赖是最佳实践
cmake_minimum_required(VERSION 3.14)
project(GTestGMockDemo)
set(CMAKE_CXX_STANDARD 14)
# 启用测试
enable_testing()
# 包含头文件目录
include_directories(include)
# 使用 FetchContent 自动获取 Googletest 和 Googlemock
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
FetchContent_MakeAvailable(googletest)
# 创建可执行文件
add_executable(unit_tests
src/user_service.cpp
tests/user_service_test.cpp
)
# 链接库
target_link_libraries(unit_tests
gtest
gmock
gtest_main
)
# 将可执行文件注册为测试
gtest_discover_tests(unit_tests)
-
测试策略 :这个示例展示了如何通过Mock隔离依赖,从而可以集中测试
UserService::RegisterUser自身的逻辑(参数校验和流程控制)。 -
ON_CALL与EXPECT_CALL:EXPECT_CALL用于设置期望 ,要求调用必须发生。而ON_CALL用于设置默认行为,不强制要求调用发生,适合为那些非测试重点的依赖方法提供默认返回值。 -
参数匹配器 :示例中使用了
testing::_,这是一个通配符,可以匹配任何参数。GMock 还提供了丰富的匹配器,如testing::Gt(大于)、testing::StartsWith(字符串起始匹配)等,可以让你更精确地控制预期