Google Test表格驱动测试最佳实践案例
案例一:用列表组织测试用例
在使用 Google Test 进行表驱动测试时,遵循最佳实践可以确保测试代码的可读性、可维护性和扩展性。以下是一些最佳实践建议:
1. 组织测试数据
将测试数据和预期结果放在一个结构体或类中,以便于管理和扩展。
2. 使用 TestWithParam
和 INSTANTIATE_TEST_SUITE_P
TestWithParam
可以让你轻松地将参数化测试与测试数据关联。确保使用最新的 Google Test 版本以利用其新功能。
3. 明确测试名称
使用 INSTANTIATE_TEST_SUITE_P
时,确保测试名称和参数名称明确,这有助于调试和理解测试输出。
4. 使用自定义比较函数
对于复杂的比较,可以编写自定义比较函数来提高测试的清晰度。
示例
以下是一个完整的表驱动测试示例,包括组合生成函数和最佳实践。
组合生成函数
#include <vector>
template <typename T>
void GenerateCombinations(const std::vector<T>& elements, int start, int remaining, std::vector<T>& currentCombination, std::vector<std::vector<T>>& result) {
if (remaining == 0) {
result.push_back(currentCombination);
return;
}
for (int i = start; i <= elements.size() - remaining; ++i) {
currentCombination.push_back(elements[i]);
GenerateCombinations(elements, i + 1, remaining - 1, currentCombination, result);
currentCombination.pop_back();
}
}
template <typename T>
std::vector<std::vector<T>> GetAllCombinations(const std::vector<T>& elements) {
int N = elements.size();
std::vector<std::vector<T>> allCombinations;
for (int k = 0; k <= N; ++k) {
std::vector<T> currentCombination;
GenerateCombinations(elements, 0, k, currentCombination, allCombinations);
}
return allCombinations;
}
表驱动测试
#include <gtest/gtest.h>
#include <vector>
#include <algorithm>
// 声明组合生成函数
template <typename T>
std::vector<std::vector<T>> GetAllCombinations(const std::vector<T>& elements);
// 组合测试用例结构体
struct CombinationTestCase {
std::vector<int> input;
std::vector<std::vector<int>> expected;
};
// 自定义比较函数
bool CompareCombinations(const std::vector<std::vector<int>>& a, const std::vector<std::vector<int>>& b) {
auto sorted_a = a;
auto sorted_b = b;
for (auto& combination : sorted_a) {
std::sort(combination.begin(), combination.end());
}
std::sort(sorted_a.begin(), sorted_a.end());
for (auto& combination : sorted_b) {
std::sort(combination.begin(), combination.end());
}
std::sort(sorted_b.begin(), sorted_b.end());
return sorted_a == sorted_b;
}
// 测试类
class CombinationTest : public ::testing::TestWithParam<CombinationTestCase> {};
TEST_P(CombinationTest, GeneratesCorrectCombinations) {
auto testCase = GetParam();
auto result = GetAllCombinations(testCase.input);
EXPECT_TRUE(CompareCombinations(result, testCase.expected));
}
// 定义测试数据
CombinationTestCase testCases[] = {
{
{1, 2, 3},
{
{}, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}
}
},
{
{1, 2},
{
{}, {1}, {2}, {1, 2}
}
},
{
{1},
{
{}, {1}
}
},
{
{},
{
{}
}
}
};
// 实例化测试用例
INSTANTIATE_TEST_SUITE_P(CombinationTests, CombinationTest, ::testing::ValuesIn(testCases));
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
解释
组合生成函数的实现:代码提供了生成所有组合的
GenerateCombinations
和GetAllCombinations
函数。表驱动单元测试:
组合测试用例结构体:
CombinationTestCase
存储了输入和预期结果。自定义比较函数:
CompareCombinations
用于对比两个组合结果,确保在比较前对每个组合和整体结果进行排序。测试类:
CombinationTest
继承自::testing::TestWithParam<CombinationTestCase>
。测试逻辑:在
TEST_P
宏中定义,通过GetParam
获取测试参数,并使用自定义比较函数CompareCombinations
验证结果。定义测试数据:
testCases
包含输入和期望结果。实例化测试用例:
INSTANTIATE_TEST_SUITE_P
将测试数据与测试用例关联。
总结
遵循上述最佳实践,可以提高测试代码的可读性和可维护性,使得添加和管理测试数据更加方便。通过使用自定义比较函数,可以更清晰地表达复杂的比较逻辑,确保测试的准确性。
案例二:从文件加载测试用例
Google Test(通常称为gtest)是一个用于C++的测试框架,它提供了丰富的测试功能,包括参数化测试。如果你想要在使用Google Test时在main
函数之前从文件中加载测试用例,你可以考虑使用参数化测试和环境设置(Environment)来实现。
以下是一个简单的步骤说明,展示如何实现这一目标:
定义参数化测试: 首先,你需要定义一个参数化测试。这通常涉及到创建一个测试参数结构体,一个参数化测试的工厂函数,以及一个测试模板。
从文件读取测试数据: 在
main
函数之前,你可以创建一个Environment
对象,它在测试开始前运行。在这个环境设置中,你可以从文件中读取测试数据,并将这些数据作为参数传递给测试。注册测试: 使用从文件中读取的数据注册参数化测试。
下面是一个简化的代码示例:
#include <fstream>
#include <gtest/gtest.h>
using ::testing::TestWithParam;
using ::testing::ValuesIn;
// 测试参数结构体
struct TestCase {
int input;
int expected;
};
// 测试环境,用于从文件加载测试用例
class TestCaseEnvironment : public ::testing::Environment {
public:
void SetUp() override {
std::ifstream file("test_cases.txt");
if (!file) {
std::cerr << "Cannot open test cases file." << std::endl;
exit(1);
}
TestCase test_case;
while (file >> test_case.input >> test_case.expected) {
test_cases.push_back(test_case);
}
}
const std::vector<TestCase>& GetTestCases() const {
return test_cases;
}
private:
std::vector<TestCase> test_cases;
};
// 参数化测试
class MyTest : public TestWithParam<TestCase> {};
TEST_P(MyTest, TestFunction) {
const TestCase& test_case = GetParam();
// 执行测试逻辑...
EXPECT_EQ(/* 计算结果 */, test_case.expected);
}
// 从环境获取测试用例并注册测试
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
TestCaseEnvironment env;
::testing::AddGlobalTestEnvironment(&env);
std::vector<TestCase> test_cases = env.GetTestCases();
::testing::GTEST_FLAG(GetArgvs().c_str()) = ValuesIn(test_cases);
return RUN_ALL_TESTS();
}
在这个例子中,TestCaseEnvironment
类负责从文件中读取测试用例,并在测试开始前进行设置。MyTest
是一个参数化测试类,它使用TEST_P
宏定义测试。在main
函数中,我们通过TestCaseEnvironment
获取测试用例,并将它们注册到测试中。
请注意,这个例子是一个简化的示例,实际使用时可能需要根据你的具体需求进行调整。此外,确保你的测试用例文件(在这个例子中是test_cases.txt
)格式正确,并且与代码中的读取逻辑相匹配。
案例三:参数化测试的执行过程
在使用 GoogleTest(gtest)进行参数化测试时,可以通过从文件中读取测试用例并将其传递给参数化测试的方式来加载测试数据。这通常涉及以下步骤:
编写测试数据文件:创建一个包含测试用例的文件,通常使用CSV或JSON格式。文件内容应该能够被程序轻松解析。
读取文件并解析数据:使用C++代码读取文件内容并解析为所需的结构,例如一个向量或其他容器,以便用作测试数据。
自定义参数生成器:为GoogleTest的参数化测试定义一个自定义参数生成器函数。这个函数将读取文件内容并返回一个表示测试参数的容器(如
std::vector
)。使用
INSTANTIATE_TEST_SUITE_P
宏:使用INSTANTIATE_TEST_SUITE_P
宏将自定义参数生成器绑定到测试用例。
以下是一个示例,演示如何从文件加载CSV格式的测试用例:
示例代码
步骤1:准备CSV测试数据文件(test_cases.csv
)
input,expected_output
1,2
2,3
3,4
步骤2:读取文件并解析数据
#include <gtest/gtest.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
// 定义一个结构来表示单个测试用例
struct TestCase {
int input;
int expected_output;
};
// 从CSV文件加载测试用例
std::vector<TestCase> LoadTestCasesFromFile(const std::string& filename) {
std::vector<TestCase> test_cases;
std::ifstream file(filename);
std::string line;
// 跳过CSV头行
std::getline(file, line);
while (std::getline(file, line)) {
std::stringstream ss(line);
std::string input_str, expected_output_str;
std::getline(ss, input_str, ',');
std::getline(ss, expected_output_str, ',');
TestCase test_case;
test_case.input = std::stoi(input_str);
test_case.expected_output = std::stoi(expected_output_str);
test_cases.push_back(test_case);
}
return test_cases;
}
步骤3:定义参数化测试
class MyParamTest : public ::testing::TestWithParam<TestCase> {
};
TEST_P(MyParamTest, TestFunction) {
TestCase test_case = GetParam();
// 假设有一个函数叫做MyFunction
int result = MyFunction(test_case.input);
EXPECT_EQ(result, test_case.expected_output);
}
步骤4:实例化参数化测试
INSTANTIATE_TEST_SUITE_P(
FromFile,
MyParamTest,
::testing::ValuesIn(LoadTestCasesFromFile("test_cases.csv"))
);
解释
LoadTestCasesFromFile
:读取CSV文件并将每行解析为一个TestCase
对象。TEST_P
:定义参数化测试的测试体。INSTANTIATE_TEST_SUITE_P
:实例化参数化测试,使用ValuesIn
从文件中加载测试数据。
通过这种方式,你可以从文件加载测试用例,并使用GoogleTest的参数化测试功能来进行测试。
下面是将上述代码整合到一个完整的 C++ 文件中的示例。这段代码演示了如何使用 GoogleTest 的参数化测试从文件加载测试用例,并进行测试。
完整示例代码(main.cpp
)
#include <gtest/gtest.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
// 定义一个结构来表示单个测试用例
struct TestCase {
int input;
int expected_output;
};
// 假设要测试的函数
int MyFunction(int x) {
return x + 1; // 示例实现
}
// 从CSV文件加载测试用例
std::vector<TestCase> LoadTestCasesFromFile(const std::string& filename) {
std::vector<TestCase> test_cases;
std::ifstream file(filename);
std::string line;
// 检查文件是否成功打开
if (!file.is_open()) {
std::cerr << "Unable to open file: " << filename << std::endl;
return test_cases;
}
// 跳过CSV头行
std::getline(file, line);
while (std::getline(file, line)) {
std::stringstream ss(line);
std::string input_str, expected_output_str;
std::getline(ss, input_str, ',');
std::getline(ss, expected_output_str, ',');
TestCase test_case;
test_case.input = std::stoi(input_str);
test_case.expected_output = std::stoi(expected_output_str);
test_cases.push_back(test_case);
}
return test_cases;
}
// 定义参数化测试类
class MyParamTest : public ::testing::TestWithParam<TestCase> {
};
TEST_P(MyParamTest, TestFunction) {
TestCase test_case = GetParam();
int result = MyFunction(test_case.input);
EXPECT_EQ(result, test_case.expected_output);
}
// 实例化参数化测试
INSTANTIATE_TEST_SUITE_P(
FromFile,
MyParamTest,
::testing::ValuesIn(LoadTestCasesFromFile("test_cases.csv"))
);
// 主函数:运行所有测试
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
编译和运行
准备测试数据文件:在与
main.cpp
同一目录下创建一个名为test_cases.csv
的文件,内容如下:input,expected_output 1,2 2,3 3,4
编译代码:确保你已经安装了GoogleTest库,并使用以下命令编译代码:
g++ -std=c++11 -isystem /path/to/googletest/include -pthread main.cpp -L/path/to/googletest/lib -lgtest -lgtest_main -o test_executable
请将
/path/to/googletest/include
和/path/to/googletest/lib
替换为你本地GoogleTest库的实际路径。运行测试:编译成功后,运行生成的可执行文件:
./test_executable
你应该会看到类似于以下的测试输出:
[==========] Running 3 tests from 1 test suite. [----------] Global test environment set-up. [----------] 3 tests from FromFile/MyParamTest [ RUN ] FromFile/MyParamTest.TestFunction/0 [ OK ] FromFile/MyParamTest.TestFunction/0 (0 ms) [ RUN ] FromFile/MyParamTest.TestFunction/1 [ OK ] FromFile/MyParamTest.TestFunction/1 (0 ms) [ RUN ] FromFile/MyParamTest.TestFunction/2 [ OK ] FromFile/MyParamTest.TestFunction/2 (0 ms) [----------] 3 tests from FromFile/MyParamTest (0 ms total) [----------] Global test environment tear-down [==========] 3 tests from 1 test suite ran. (0 ms total) [ PASSED ] 3 tests.
这样,你就完成了一个完整的、可以从文件加载测试用例的 GoogleTest 参数化测试的示例。
INSTANTIATE_TEST_SUITE_P
宏的执行是在 main
函数之前完成的。具体来说,INSTANTIATE_TEST_SUITE_P
在编译期生成一组测试用例,并在程序启动时进行初始化。这意味着在进入 main
函数之前,GoogleTest 会初始化所有测试套件和测试用例。
详细解释
在 C++ 中,INSTANTIATE_TEST_SUITE_P
宏定义一个全局的测试用例实例化结构,该结构在程序加载时会自动初始化。因此,这个宏的执行顺序如下:
编译期生成测试代码:在编译期,
INSTANTIATE_TEST_SUITE_P
宏会生成所有参数化测试的代码,并将这些测试用例添加到 GoogleTest 的测试注册表中。程序启动时初始化:当程序运行时(在进入
main
函数之前),所有全局或静态变量和对象会被初始化,包括由INSTANTIATE_TEST_SUITE_P
宏生成的测试用例。这是因为 GoogleTest 会在main
函数执行之前注册所有的测试。运行
main
函数:在测试用例注册和初始化完成后,main
函数才会被调用。在main
函数中,::testing::InitGoogleTest(&argc, argv);
会再次初始化 GoogleTest 的运行环境,之后RUN_ALL_TESTS()
函数会执行所有注册的测试用例。
代码执行顺序示意
以下是一个简化的顺序图,说明了代码执行的顺序:
全局/静态对象初始化阶段
INSTANTIATE_TEST_SUITE_P
宏生成的代码会在这个阶段执行。测试用例被注册到 GoogleTest 框架中。
进入
main
函数调用
::testing::InitGoogleTest(&argc, argv);
初始化 GoogleTest 的运行环境。调用
RUN_ALL_TESTS()
执行所有注册的测试用例。
结论
INSTANTIATE_TEST_SUITE_P
的执行确实发生在 main
函数之前,确保在进入 main
函数时,所有测试用例都已经被正确注册并准备好执行。
Subscribe to my newsletter
Read articles from wuzhiguocarter directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
wuzhiguocarter
wuzhiguocarter
I am a senior software engineer working on DiDi.