【发布时间】:2014-05-24 23:27:12
【问题描述】:
出于单元测试的目的,我需要模拟一个网络响应。响应通常是字节流,存储为const vector<uint8_t>。然而,对于单元测试,我想使用在 CPP 文件中硬编码或从同一解决方案中的文件读取的数据生成向量。我的示例数据约为 6 kb。使用googletest 时,关于在何处放置数据的一般指导是什么?
【问题讨论】:
标签: c++ unit-testing googletest stubs
出于单元测试的目的,我需要模拟一个网络响应。响应通常是字节流,存储为const vector<uint8_t>。然而,对于单元测试,我想使用在 CPP 文件中硬编码或从同一解决方案中的文件读取的数据生成向量。我的示例数据约为 6 kb。使用googletest 时,关于在何处放置数据的一般指导是什么?
【问题讨论】:
标签: c++ unit-testing googletest stubs
(警告 - 此答案适用于任何单元测试框架)
我更喜欢将测试数据文件作为单独的对象保存在版本控制系统中。这提供了以下好处:
如果您不希望单元测试执行读取数据文件,这在某些情况下可能是必要条件,您可以选择编写一个程序或脚本来生成在夹具设置时初始化向量的 C++ 代码。
【讨论】:
也许 (a) 您需要为某个角色提供大量数据序列
测试用例只会阅读它。这也可能是(类)全局数据,具有
const 访问。
也许 (b) 您需要为某个角色提供大量数据序列
测试用例将读取并修改或销毁它。这需要
每个测试用例重新初始化,并且具有非const 访问权限。
也许两者兼而有之。在任何一种情况下,传统的 googletest 实现都会
使用test fixture
封装数据的获取,将在
夹具的虚拟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),您还可以考虑获取global SetUp 中的数据
通过子类化::testing::Environment 的成员函数,但我看不到
喜欢这样做的一般原因。
...或者硬编码?
然后是把测试数据保存在文件中还是硬编码的问题 它在测试源中。 在这一点上快乐的读者从现在开始只会感到无聊。
作为一个普遍问题,这是一个在情况下判断的问题,我不认为 googletest 的使用大大提高了天平。我认为主要考虑 是:是否希望能够在没有的情况下改变测试数据项? 重建测试套件?
说重建测试套件以改变这个项目是一个不可忽略的成本,你
预计该项目的内容将在未来独立变化
相关的测试代码。或者它可以变化,独立于相关的测试代码,
用于被测系统的不同配置。在这种情况下,最好得到
可以通过运行时参数选择的文件或其他源中的项目
测试套件。在 googletest 中,子类化 class ::testing::Environment 是
为参数化获取测试套件资源而设计的工具。
如果在现实中测试数据项的内容是松散耦合的 关联的测试代码,然后将其硬编码到测试用例中是最不可能的 成为一个审慎的选择。 (和测试文件,而不是其他类型的运行时 配置器,具有可以在其中进行版本控制的宝贵属性 与源代码相同的系统。)
如果测试数据项的内容与 关联的测试代码然后我倾向于硬编码而不是提取它 从数据文件。只是有偏见,而不是教条主义的承诺。也许你的测试 套件采用强大的库设施来初始化公共 API 测试数据, 比如说,也与测试管理和缺陷管理挂钩的 XML 文件, 系统。很好!
我认为如果一个测试数据文件是主要的 测试资源 - 测试套件无法生成的资源 - 然后是其内容 最好是有能力的维护者容易理解的文本数据 和操纵。在这种情况下,我当然会认为 例如,C/C++ 十六进制常量列表是 文本数据 - 它是 源代码。如果测试文件包含二进制或令人生畏的面向机器的数据 那么测试套件最好包含其生产方式,使其清晰易读 初级资源。测试套件有时不可避免地要依赖 外部来源的“原型”二进制文件,但它们几乎不可避免地需要 测试工程师和错误修复者在十六进制编辑器面前变成灰色的可怕景象。
鉴于主要测试数据应该对维护者清晰易读的原则,我们 可以将主要测试数据将是“某种代码”作为一种规范:它将 是无逻辑的,但它将是程序员的那种文本内容 习惯于测量和编辑。
想象一个由 4096 个 64 位无符号整数组成的特定序列 (Big Magic Table)是测试您的软件所必需的,并且非常严格 结合相关的测试代码。它可以被硬编码为一个巨大的向量或数组 测试套件的某些源文件中的初始化程序列表。它可能是 由测试套件从以 CSV 格式或以 CSV 格式维护的数据文件中提取 CSV 标点行。
对于从数据文件中提取和反对硬编码,可以敦促 (根据 Andrew McDonell 的回答)这很有价值地解开了 从同一代码中其他代码的修订版本对 BMT 的修订 源文件。同样,可能会敦促任何框架的源代码 巨大的文字初始化往往无法测量,因此需要维护 责任。
但是这两点都可以用定义的观察来反驳
BMT 的声明可能会单独编码在一个源文件中。它
可能是测试数据初始化的测试套件的代码审查策略
必须如此编码 - 并且可能在遵循独特命名的文件中
习俗。可以肯定的是,这是一项狂热的政策,但并不比
一种坚持必须从文件中提取所有测试数据初始化程序的方法。
如果维护者有义务在包含它的任何文件中调查 BMT,
文件扩展名是.cpp、.dat 或
不管怎样:所有的问题都是“代码”的可理解性。
对于硬编码和反对从数据文件中提取,可以敦促 从数据文件中提取必须引入不相关的来源 测试用例的潜在失败 - 所有不应该发生错误 可能无法从文件中读取正确的数据。这会增加开销 测试开发以在真正的测试失败和真正的测试失败之间产生正确和清晰的区别 未能从文件中获取测试数据,并清楚地诊断所有可能的 后者的原因。
对于 googletest 和类似功能的框架,这一点可以
在一定程度上通过多态夹具基类进行反驳
比如::testing::Test 和::testing::Environment。这些有利于
测试开发者在测试用例中封装测试资源的获取
或测试套件初始化,以使其全部结束,无论是成功还是
在诊断出故障的情况下,在运行任何测试用例的组成测试之前。
RAII 可以在设置失败和实际失败之间保持一个没有问题的划分。
尽管如此,数据文件的文件处理开销是不可减少的 路由和框架的 RAII 特性存在操作开销 什么都不做减少。在我与大量交易的测试系统打交道时 数据文件,数据文件只是比 只需要在构建时存在且正确的源文件。 数据文件更有可能在运行时丢失或放错位置,或者 包含格式错误的内容,或以某种方式被拒绝许可,或 以某种方式出现在错误的版本中。它们在测试系统中的用途 不像源文件那样简单或严格控制。 东西 测试数据文件不应该发生这种情况是 测试依赖它们并与它们的数量成正比的系统。
由于源文件可以卫生地封装测试数据初始化 对于修订跟踪,对它们进行硬编码可以等同于从 文件,预处理器将提取作为编译的副产品。 有鉴于此,为什么要使用其他具有额外责任的机器来提取它? 可能会有很好的答案,比如建议的带有测试管理的 XML 接口, 缺陷管理系统,但“这是测试数据,所以不要硬编码”不是一个好方法。
即使测试套件必须支持以下系统的各种配置 调用测试数据项的各种实例化的测试,如果数据 item 与 测试套件的构建配置 一致,您可以 仍然(卫生地)硬编码它并让条件编译选择 正确的硬编码。
到目前为止,我还没有质疑修订跟踪卫生论点 用于测试数据初始值设定项的基于文件的隔离。我刚刚做了 指出对初始化程序进行硬编码的常规源文件可以 完成这个隔离。我不想推翻这个论点,但是 我想阻止它得出测试数据初始化程序的狂热结论 原则上应始终从专用文件中提取 - 无论是源文件还是数据文件。
没有必要详细说明反对这个结论的原因。那样 测试代码在本地比一般吃披萨的人更难理解 程序员会编写和组织不断增长的测试套件文件 令人难以置信的速度比必要或健康的要快得多。规范地, 测试套件的所有主要资源都是“某种代码”。一种 程序员的技能包括将代码划分为文件的技能 具有适当的粒度以确保适当的修订跟踪卫生。 这不是一个机械程序,而是一种专业知识,需要代码审查来涵盖。 代码审查可以而且应该确保测试数据初始化,但是 他们已经完成,经过精心设计和精心制作,与修订跟踪相比 就像在所有其他常规方面一样。
底线:如果您希望能够针对不同的测试套件运行相同的构建版本 这些模拟网络响应,从文件中读取。另一方面,如果它 与测试套件的构建配置保持不变或协变,为什么不很难 编码?
【讨论】: