我正在对现有代码进行单元测试,而编写该代码时并未考虑单元测试。
有一些这样的类结构:
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()
}
之所以将它们保留为私有是因为它的设计方式是只有Model类可以调用它们,因此是朋友。
[在实际的代码中,有一个Attorney-Client惯用法,将Model的访问权限限制为此类,但我不在代码段之内]
现在,我正在尝试对类进行单元测试。在弄清楚如何测试这些私有功能的同时,我遇到了Iceberg Class的这种术语,我认为上述类是有罪的。
大多数涉及该主题的文章还提到,如果需要测试私有功能,则主要意味着类已经超负荷使用,而在另一个独立的类中,这些功能最好保持公开状态。
现在,我不确定这是否是一个不好的代码设计,我应该重新设计它们以使单元测试更容易,或者我只是直接进行单元测试。
想听听您的意见
答案 0 :(得分:1)
为了使该代码可测试,我将介绍三个纯虚拟接口(ITexture
,IMesh
,IMaterial
),并添加一个免费方法来创建此类接口(例如{{ 1}}),将返回类型为getTexture
的smart_ptr。然后,在cpp文件中实现ITexture
方法,并在生产代码中使用它来创建get[...]
对象。在单元测试中,我将为每个接口类创建一个模拟并为注入的模拟设置适当的期望值(例如,使用Model
或编写自己的模拟)。
gmock
,头文件IMesh.hpp的示例:
Mesh
implementaiton文件MeshImpl.cpp:
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*/);
依赖注入:
#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
的不同实现,并在需要时将其注入模型。有关DI技术的更多详细信息,请参见this question
答案 1 :(得分:0)
我认为在这种情况下使用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
的界面仍然相同。不过,我认为您绝对需要扩展测试套件以实现新的实现:对于两个只有四个字节对齐的指针,对于只有一个指针是四个字节对齐的情况,您应该具有测试用例。指针都是四字节对齐的,并且要复制的字节数是四的倍数,并且它们不是四的倍数的情况,依此类推。也就是说,测试套件将得到极大的扩展-仅因为实现详细信息已更改。 需要新测试来查找新实现中的错误-尽管所有测试仍可以使用公共API,即memcpy
函数。
因此,假设单元测试与实现细节无关,这是错误的,并且认为测试不是特定于实现的仅仅是错误的,因为它们是通过公共API测试的。
但是,正确的是,测试不应不必要依赖于实现细节。始终首先尝试创建与实现无关的有用测试,然后再添加特定于实现的测试。对于后者,测试私有方法(例如来自friend
测试类)也可以是一个有效的选项-只要您知道其缺点(如果重命名,删除私有方法,则需要维护测试代码)等),并权衡优势。