冰山类和谷歌单元测试
Iceberg Class and Google Unit Testing
我正在经历对现有代码进行单元测试的过程,该代码的编写没有考虑单元测试。
有一些类的结构如下:
class Texture
{
public:
friend class Model;
private:
void Load( int a, int b);
void Update(int a, int b);
void Use(int a, int b);
}
class Material
{
public:
friend class Model;
private:
void Load(int a);
void Update(int a);
void Use(int a);
}
class Mesh
{
public:
friend class Model;
private:
void Load(int a, int b, int c);
void Update(int a, int b, int c);
void Use(int a, int b, int c);
}
class Model
{
public:
void Load(); // call all the individual Load()
void Use(); // call all the individual Use()
}
它们之所以保持私有,是因为它是以只有模型类可以调用它们的方式设计的,因此是朋友。
[在实际代码中,有一个律师-客户习惯用法,它限制了模型对这些类的访问,但我将其排除在代码片段之外]
现在我正在尝试为类进行单元测试。在弄清楚如何测试这些私人功能时,我遇到了冰山类的这个术语,我觉得上面的类在某种程度上是有罪的。
大多数涉及这个主题的文章还提到,如果需要测试私有函数,这主要意味着类做得过头了,这些函数最好放在另一个独立的类中,在那里它们保持公共状态。
所以现在,我不确定这是否是一个糟糕的代码设计,我应该重新设计它们以使单元测试更容易,或者我只是按原样进行单元测试。
想听听您的意见
为了使这段代码可测试,我将引入三个纯虚拟接口(ITexture
、IMesh
、IMaterial
),并添加一个自由方法来创建这样的接口(例如getTexture
),这将返回类型ITexture
的smart_ptr。然后在 cpp 文件中实现一个get[...]
方法,并在生产代码中使用它来创建Model
对象。在单元测试中,我会为每个接口类创建一个模拟,并对注入的模拟设置适当的期望(例如,使用gmock
或编写自己的模拟)。
Mesh
、头文件、IMesh.hpp 的示例:
class IMesh {
public:
virtual ~IMesh() = default;
virtual void Load(int a, int b, int c) = 0;
virtual void Update(int a, int b, int c) = 0;
virtual void Use(int a, int b, int c) = 0;
};
std::unique_ptr<MeshI> getMesh(/*whatever is needed to create mesh*/);
implementaiton file, MeshImpl.cpp:
#include "IMesh.hpp";
class Mesh : public IMesh {
public:
Mesh(/*some dependency injection here as well if needed*/);
void Load(int a, int b, int c) override;
void Update(int a, int b, int c) override;
void Use(int a, int b, int c) override;
};
Mesh::Mesh(/*[...]*/) {/*[...]*/}
void Mesh:Load(int a, int b, int c) {/*[...]*/}
void Mesh:Update(int a, int b, int c) {/*[...]*/}
void Mesh:Use(int a, int b, int c) {/*[...]*/}
依赖注入:
Model model{getMesh(), getTexture(), getMaterial()};
通过这种方法,可以实现:
- 更好的解耦 - 友谊是一种非常强大的耦合机制,而依赖纯虚拟接口是一种常见的方法)
- 更好的可测试性 - 不仅对于
Model
类 - 因为接口中的所有方法都必须public
才能Model
类使用它,您现在可以单独测试每个接口 - 更好的封装:只能通过getter方法创建所需的类 - 用户无法访问实现,所有私有内容都是隐藏的。
- 更好的可扩展性:现在用户可以提供不同的
IMesh
实现,并在需要时将其注入模型。
有关 DI 技术的更多详细信息,请参阅此问题
我认为在这种情况下使用friend
是不幸的。 在我看来,friend
的一个很好的用例是,允许在概念上具有紧密耦合的类之间访问私有元素。 当我写它们在概念上具有紧密耦合时,我的意思是紧密耦合不是使用friend
的结果,但是这些类之间的紧密耦合是由于它们的依赖性,这是它们定义角色的结果。 在这种情况下,friend
是一种正确处理这种紧密耦合的机制。 例如,容器及其相应的迭代器类在概念上是紧密耦合的。
在您的情况下,在我看来,这些类在概念层面上并没有那么紧密耦合。 您将friend
用于不同的目的,即强制执行架构规则:只有Model
才能使用方法Load
、Update
和Use
。 不幸的是,这种模式有局限性:如果你有另一个类Foo
和第二个架构规则,Foo
只能被允许调用Use
方法,你不能同时表达这两个架构规则:如果你也Foo
其他类的朋友,那么Foo
不仅可以访问Use
, 但也要Load
和Update
- 您不能以精细的方式授予访问权限。
如果我的理解是正确的,那么我会争辩说Load
、Update
和Use
在概念上不是private
的,也就是说,它们不代表应该为外部隐藏的类的实现细节:它们属于类的"官方"API,只是附加规则只有Model
才能使用它们。 通常,private
方法是私有的,因为实现者希望保留重命名或删除它们的自由,因为其他代码无法访问它们。 我认为,这不是这里的意图。
考虑到这一切,我认为最好以不同的方式处理这种情况。 公开Load
方法,Update
和Use
,并添加注释以解释体系结构约束。 而且,尽管我的论点不是关于可测试性的,但这也解决了您的测试问题之一,即允许您的测试也访问Load
、Update
和Use
。
如果你还希望能够模拟你的类Texture
、Material
和Mesh
,那么考虑Quarra
的建议来引入各自的接口。
尽管对于您的具体示例,我的建议是将方法Load
、Update
和Use
公开,但我并不反对单元测试实现细节。 同一接口的替代实现具有不同的潜在错误。 而且,发现错误是测试的一个主要目标(参见Myers,Badgett,Sandler:软件测试的艺术,或Beizer:软件测试技术等)。
例如,考虑memcpy
函数:假设您必须实现和测试它。 您从一个简单的解决方案开始,逐个字节复制,然后对其进行彻底测试。 然后,您意识到,对于 32 位计算机,如果源地址和目标地址是 32 位对齐的,则可以做得更快:在这种情况下,您可以一次复制四个字节。 当您实现此更改时,新memcpy
在内部看起来完全不同:首先检查指针对齐方式是否合适。 如果不适合,则执行原始的逐字节复制,否则执行更快的复制例程(这也必须处理字节数不是四的倍数的情况,因此最后可能会有一些额外的字节要复制)。
memcpy
的界面仍然相同。 尽管如此,我认为您肯定需要为新实现扩展测试套件:您应该为两个四字节对齐的指针提供测试用例,对于只有一个指针是四字节对齐的情况等。 您需要指针都是四字节对齐且要复制的字节数是 4 的倍数的情况,以及它们不是 4 的倍数的情况,等等。 也就是说,您的测试套件将大幅扩展 - 只是因为实现细节发生了变化。需要新的测试来查找新实现中的错误 - 尽管所有测试仍然可以使用公共API,即memcpy
函数。
因此,假设单元测试与实现细节无关是错误的,并且仅仅因为它们通过公共 API 进行测试而假设测试不是特定于实现的也是错误的。
但是,测试不应不必要地依赖于实现细节,这是正确的。 始终首先尝试创建与实现无关的有用测试,然后再添加特定于实现的测试。 对于后者,测试私有方法(例如来自friend
测试类)也可以是一个有效的选择 - 只要您意识到缺点(如果私有方法被重命名、删除等,则需要维护测试代码)并权衡它们与优点。
- 谷歌测试中的期望值存储在哪里
- 更正 CMakeList.txt 用于谷歌测试的文件?
- 如何让谷歌测试正常运行。测试总是失败。(它不会编译)
- 谷歌测试方法与流
- _mm256_load_ps调试模式下导致谷歌/基准测试的分段错误
- 谷歌测试:模板模板的笛卡尔乘积的类型列表与模板
- C++ 谷歌测试除以零
- 如何使用谷歌基准测试对自定义界面进行基准测试
- 谷歌基准测试,如何只调用一次代码?
- 多定义C ++ / C谷歌测试
- 谷歌测试可执行文件中的单元测试未被vstest选中
- C++ 谷歌模拟/单元测试:模拟方法未调用,原始方法是
- 冰山类和谷歌单元测试
- 谷歌单元测试框架的链接器错误
- 如何在Xcode 6.1.1中使用谷歌测试运行本地c++单元测试
- 使用谷歌测试同时为多个测试模块运行单元测试
- 谷歌模拟单元测试
- 数据驱动的单元测试与谷歌测试
- 多次调用单个测试-谷歌测试
- Visual Studio C++:单元测试exe项目与谷歌测试