检测c++结构从单元测试更新

Detecting c++ structure is updated from unit test

本文关键字:单元 测试更新 结构 c++ 检测      更新时间:2023-10-16

我有一组数据结构,应该使用boost::序列化从一层传递到另一层。例如

struct DataType1
{
    std::string field1;
    std::string field2;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & field1;
        ar & field2;
    }
};

我想写单元测试,只是为了确保我没有错过一些字段(有很多结构和字段)。

问题是,如果我在结构中添加新字段(我肯定会这样做)并忘记更新单元测试,则该字段将不会被单元测试覆盖。

我的问题是:如何检测结构(或类)被改变。我的想法是使用static_assert(sizeof(DataType1) == HARD_CODED_VALUE),但它在不同的编译器,平台(x64, x86)和配置(发布,调试)中遭受结构大小的差异。

有什么好主意吗?

问题是,如果我在结构中添加新字段(我肯定会这样做)并且忘记更新单元测试,该字段将不会被单元测试覆盖。

我的问题是:如何检测结构(或类)被改变。

我的想法是使用static_assert(sizeof(DataType1) == HARD_CODED_VALUE)[…]

这不是一个可移植的解决方案(正如你自己所指出的)。

有什么好主意吗?

是的:你能从更新测试开始吗?

也就是说,不要决定结构中应该包含什么,然后添加它,然后更新测试(如果您没有忘记)。

相反,更新测试以检查新的序列化数据,然后确保更新的测试失败,然后更新代码以使测试通过。

这种方法(首先编写/更新单元测试)已经(部分地)被创建来解决这个问题。

测试优先的方法还有其他优点:

  • 它巧妙地避免了YAGNI

  • 它最小化过早优化

  • 它自然演变为跟踪应用程序/实现的功能完整性。

向类定义添加注释,以提醒您在添加成员时必须调整序列化器。计算机能为你做的事情是有限的——这就是为什么代码审查很重要。让任何补丁由另一个程序员审查,有一组严格的测试用例,并希望最好的。

我相信你可以写一个clang插件,它可以确保一个特定的方法引用一个结构体的每个成员,但是你真的需要这个吗?你能把你的时间花在这个上面吗?

也就是说,如果你尝试将尽可能多的工作卸载到计算机上,你会得到额外的积分。即使是static_assert的把戏也是个好把戏。如果您使用一组针对您经常构建的特定ABI和体系结构的#ifdef来保护它,它可能会做得很好。

怎么样?

// DataType1_members.h
FIELD_DEF(std::string, field1);
FIELD_DEF(std::string, field2);
// DataType1.h
struct DataType1
{
#define FIELD_DEF(type, name) type name
    #include "DataType1_members.h"
#undef FIELD_DEF
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
#define FIELD_DEF(type, name) ar & name
    #include "DataType1_members.h"
#undef FIELD_DEF
    }
};

这样您只需要在一个地方添加字段。

我有一个类似的问题,我在使用boost::fusion中找到了我的解决方案。在这里,您可以遍历结构体的所有成员。所以不需要再手工操作了。同时,您还可以获得编译时自省的良好特性。因此很容易打印完整结构体的内容。通过一小段模板代码转换成日志文件。

在单元测试中,您可以使用预期的结构大小进行static_assert检查:

static_assert( sizeof(DataType1)==16, "Structure changed. Update serialize method" );

您必须为每个平台(或仅为一个平台)设置结构的大小(检查中的数字)。

你可以在你的结构中添加静态变量"version",并在结构改变时增加它。

static int version = 1234;

然后在测试中写入

static_assert( DataType1::version == HARD_CODE_VALUE );
但是当你改变结构时,你仍然可能忘记更新版本,或者当你更新测试时,忘记添加一些新成员。

一个疯狂的方法是不直接使用成员。

创建聚合变量模板。创建数据成员模板

数据成员模板接受一个标记结构体。

覆盖data_member<tag,T>::operator^( tag ),返回对T的引用。也许对免费的operator^( data_member< tag, T >*, tag )做同样的事情

现在你可以通过this^tag()获得成员,这看起来像成员访问。如果你创建一个tag的全局实例,你甚至可以删除()

你也有编译时对你的数据成员的反射,所以你可以写for_each_member,并编写所有的序列化代码一次,并使用它为每个struct

访问控制和data_member的其他类别可以在aggregate模板中完成。

基于标签的数据构造可以用一个复杂而花哨的aggregate构造函数来完成。

或者你可以等待真正的反射在c++中出现,可能在十年内。

或者,您可以将struct转换为tuple包装器,并使用类似上面的覆盖技巧来使this^tag为基于名称的访问工作。

如果我们有一个包含int x, ydouble dstruct foo,我们想这样做,我们可以这样做:

// boilerplate
template<typename C, std::size_t idx> struct Tag {};
template<typename C, std::size_t tag_idx>
auto operator^(C&& lhs, Tag<C, tag_idx> const&>)
  -> decltype( std::get<tag_idx>( std::forward<C>(lhs) )
{ return std::get<tag_idx>( std::forward<C>(lhs); }
struct foo:std::tuple< int, int, double > {};
Tag< foo, 0 > x; // another annoying part: need to manually number them
Tag< foo, 1 > y; // we can avoid this via an aggregate trick, but
Tag< foo, 2 > d; // even that isn't all that pretty
int main() {
  foo bar;
  bar^x = 7;
  bar^y = 3;
  bar^d = 3.14;
}

一个(严重的)问题是两个不同的struct中的两个成员变量共享相同的"命名空间",如果它们具有相同的名称则会发生冲突。