出于单元测试的目的,我需要模拟一个网络响应。响应通常是字节流,存储为const vector<uint8_t>
. 然而,对于单元测试,我想使用在 CPP 文件中硬编码或从同一解决方案中的文件读取的数据生成向量。我的示例数据约为 6 kb。使用googletest时在何处放置数据的一般指导是什么?
2 回答
也许(a)您需要一个大序列的数据来完成某个角色,而测试用例只会读取它。这也可能是(类)全局数据,具有
const
访问权限。
也许(b)您需要大量数据用于某些角色,测试用例将在其中读取、修改或销毁它。这需要根据测试用例重新初始化并且不能const
访问。
也许两者兼而有之。在任何一种情况下,传统的 googletest 实现都将使用测试夹具
来封装数据的获取,将在夹具的虚拟Setup()
成员函数的实现中获取它,并通过夹具的 getter 方法访问它。
下面的程序说明了一个夹具,它提供了从文件中获取的每个案例的可变数据和全局常量数据。
#include <vector>
#include <fstream>
#include <stdexcept>
#include "gtest/gtest.h"
class foo_test : public ::testing::Test
{
protected:
virtual void SetUp() {
std::ifstream in("path/to/case_data");
if (!in) {
throw std::runtime_error("Could not open \"path/to/case_data\" for input");
}
_case_data.assign(
std::istream_iterator<char>(in),std::istream_iterator<char>());
if (_global_data.empty()) {
std::ifstream in("path/to/global_data");
if (!in) {
throw std::runtime_error(
"Could not open \"path/to/global_data\" for input");
}
_global_data.assign(
std::istream_iterator<char>(in),std::istream_iterator<char>());
}
}
// virtual void TearDown() {}
std::vector<char> & case_data() {
return _case_data;
}
static std::vector<char> const & global_data() {
return _global_data;
}
private:
std::vector<char> _case_data;
static std::vector<char> _global_data;
};
std::vector<char> foo_test::_global_data;
TEST_F(foo_test, CaseDataWipe) {
EXPECT_GT(case_data().size(),0);
case_data().resize(0);
EXPECT_EQ(case_data().size(),0);
}
TEST_F(foo_test, CaseDataTrunc) {
EXPECT_GT(case_data().size(),0);
case_data().resize(1);
EXPECT_EQ(case_data().size(),1);
}
TEST_F(foo_test, HaveGlobalData) {
EXPECT_GT(global_data().size(),0);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
对于情况 (a),您还可以考虑通过子类化获取全局 SetUp
成员函数中的数据::testing::Environment
,但我认为没有一般理由更喜欢这样做。
...或者硬编码?
然后是是否将测试数据保存在文件中,还是将其硬编码在测试源中的问题。在这一点上快乐的读者从现在开始只会感到无聊。
作为一个一般性问题,这是一个在情况下判断的问题,我认为使用 googletest 并没有实质性地提示规模。我认为主要考虑因素是:是否可以在不重建测试套件的情况下更改测试数据项?
假设重新构建测试套件以更改此项目是不可忽略的成本,并且您预计该项目的内容将来会独立于相关的测试代码而变化。或者,对于被测系统的不同配置,它可以独立于相关的测试代码而变化。在这种情况下,最好从测试套件的运行时参数可以选择的文件或其他源中获取项目。在 googletest 中,子类class ::testing::Environment
化是为参数化获取测试套件资源而设计的工具。
如果实际上测试数据项的内容与相关的测试代码松散耦合,那么将其硬编码到测试用例中不太可能是一个谨慎的选择。(与其他类型的运行时配置器相比,测试文件具有有价值的特性,即它们可以在与源代码相同的系统中进行版本控制。)
如果测试数据项的内容与相关的测试代码紧密耦合,那么我倾向于对其进行硬编码,而不是从数据文件中提取它。只是有偏见,而不是教条主义的承诺。也许您的测试套件使用了强大的库工具来初始化公共 API 测试数据,例如,XML 文件也与测试管理和缺陷管理系统挂钩。美好的!
我认为如果测试数据文件是主要测试资源(测试套件无法生成),那么它的内容最好是有能力的维护人员可以轻松理解和操作的文本数据。在这个设置中,我当然会认为 C/C++ 十六进制常量列表是文本数据——它是源代码。如果一个测试文件包含二进制或令人生畏的面向机器的数据,那么测试套件最好包含从清晰的主要资源中生成的方法。测试套件有时不可避免地依赖于外部来源的“原型”二进制文件,但它们几乎不可避免地会导致测试工程师和错误修复者在十六进制编辑器面前变灰的严峻景象。
鉴于主要测试数据应该对维护者易读的原则,我们可以将主要测试数据将是“某种代码”作为一种规范:它将是无逻辑的,但它将是那种文本内容程序员习惯于测量和编辑。
想象一下,测试您的软件需要一个特定的 4096 个 64 位无符号整数序列(大魔表),并且与相关的测试代码紧密结合。它可以在测试套件的某些源文件中硬编码为一个巨大的向量或数组初始化列表。它可以由测试套件从以 CSV 格式或 CSV 标点行维护的数据文件中提取。
对于从数据文件中提取和反对硬编码,可以敦促(根据 Andrew McDonell 的回答)这很有价值地实现了对 BMT 的修订与同一源文件中其他代码的修订的分离。同样,可能会敦促任何构建大量文字初始化的源代码往往是不可测量的,因此是维护责任。
但是这两点可能会与 BMT 的定义声明可能被编码在它自己的源文件中的观察相反。它可能是测试套件的代码审查策略,测试数据初始化
必须如此编码 - 并且可能在遵守独特命名约定的文件中。可以肯定的是,这是一种狂热的政策,但不会比坚持必须从文件中提取所有测试数据初始化程序的政策更狂热。如果维护人员有义务在包含它的任何文件中调查 BMT,则文件扩展名是.cpp
..dat
还是其他都没有关系:所有问题都是“代码”的可理解性。
对于硬编码和反对从数据文件中提取,可以敦促从数据文件中提取必须将不相关的潜在故障源引入测试用例——所有可能无法从文件中读取正确数据的“不应发生”错误. 这给测试开发带来了开销,以正确和清晰地区分真正的测试失败和从文件中获取测试数据的失败,并清楚地诊断后者的所有可能原因。
在 googletest 和类似功能框架的情况下,这一点可以在一定程度上通过使用多态夹具基类(如::testing::Test
和::testing::Environment
. 这些有助于测试开发人员将测试资源的获取封装在测试用例或测试套件初始化中,以便在运行任何测试用例的组成测试之前,无论成功还是诊断失败,一切都结束了。RAII 可以在设置失败和实际失败之间保持一个没有问题的划分。
然而,对于数据文件路径,存在不可减少的文件处理开销,并且框架的 RAII 特性无法减少操作开销。在我处理大量使用数据文件进行交易的测试系统时,数据文件比源文件更容易出现操作失误,源文件只需要在构建时出现并正确。数据文件更有可能在运行时丢失或放错位置,或者包含格式错误的内容,或者以某种方式被拒绝许可,或者以某种方式出现在错误的修订版中。它们在测试系统中的使用不像源文件那样简单或严格控制。不应该发生的事情测试数据文件的发生是依赖于它们的测试系统的操作摩擦的一部分,并且与它们的数量成正比。
由于源文件可以卫生地封装测试数据初始化以进行修订跟踪,因此硬编码可以等同于从文件中提取,而预处理器将提取作为编译的副产品进行。有鉴于此,为什么要使用其他具有额外责任的机器来提取它?可能会有很好的答案,例如建议的带有测试管理、缺陷管理系统的 XML 接口,但是“这是测试数据,所以不要硬编码”不是一个好答案。
即使测试套件必须支持被测系统的各种配置,这些配置需要测试数据项的各种实例化,如果数据项与测试套件的构建配置一致,您仍然可以(卫生上)硬编码它并让条件编译选择正确的硬编码。
到目前为止,我还没有对基于文件的测试数据初始化程序隔离的修订跟踪卫生参数提出质疑。我刚刚指出,初始化程序被硬编码的常规源文件可以完成这种隔离。我不想推翻这个论点,但我想阻止它得出一个狂热的结论,即测试数据初始化程序原则上应该总是从专用文件中提取——无论是源文件还是数据文件。
没有必要详细解释反对这个结论的原因。这样一来,本地的测试代码就比普通的吃披萨的程序员编写的代码更难理解,而且测试套件文件的组织比必要或健康的增长速度快得多令人难以置信。按照规范,测试套件的所有主要资源都是“某种代码”。程序员的技能包括将代码划分为具有适当粒度的文件以确保适当的修订跟踪卫生的技能。这不是一个机械程序,而是一种专业知识,需要代码审查来涵盖。代码审查可以而且应该确保测试数据初始化,无论它们是如何完成的,
底线:如果您希望能够为各种模拟网络响应运行相同构建的测试套件,请从文件中读取它。另一方面,如果它与测试套件的构建配置是不变的或协变的,为什么不对其进行硬编码呢?
(警告 - 这个答案对任何单元测试框架都是通用的)
我更喜欢将测试数据文件作为单独的对象保存在版本控制系统中。这提供了以下好处:
- 您可以编写单元测试以接受任何或多个数据文件来测试各种情况
- 您可以根据需要跟踪数据的变化
如果您不希望单元测试执行读取数据文件,这在某些情况下可能是必要条件,您可以选择编写一个程序或脚本来生成 C++ 代码,该代码在夹具设置时初始化向量。