Iceberg课堂和Google单元测试

时间:2019-06-11 03:41:38

标签: c++ unit-testing googletest friend

我正在对现有代码进行单元测试,而编写该代码时并未考虑单元测试。

有一些这样的类结构:

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的这种术语,我认为上述类是有罪的。

大多数涉及该主题的文章还提到,如果需要测试私有功能,则主要意味着类已经超负荷使用,而在另一个独立的类中,这些功能最好保持公开状态。

现在,我不确定这是否是一个不好的代码设计,我应该重新设计它们以使单元测试更容易,或者我只是直接进行单元测试。

想听听您的意见

2 个答案:

答案 0 :(得分:1)

为了使该代码可测试,我将介绍三个纯虚拟接口(ITextureIMeshIMaterial),并添加一个免费方法来创建此类接口(例如{{ 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) {/*[...]*/}

借助这种方法,您可以实现:

  1. 更好的解耦-友谊是一种非常强大的耦合机制,而依靠纯虚拟接口是一种常见的方法)
  2. 更好的可测试性-不仅适用于Model model{getMesh(), getTexture(), getMaterial()}; 类-因为接口中的所有方法都必须是Model才能使public类使用它,所以您现在可以分别测试每个接口
  3. 更好的封装:只能通过getter方法创建所需的类-用户无法访问实现,所有私有内容都被隐藏。
  4. 更好的可扩展性:现在,用户可以提供Model的不同实现,并在需要时将其注入模型。

有关DI技术的更多详细信息,请参见this question

答案 1 :(得分:0)

我认为在这种情况下使用friend是不幸的。如我所见,friend的一个很好的用例是,允许访问概念上紧密耦合的类之间的私有元素。当我从概念上说时,它们具有紧密的耦合,我的意思是紧密的耦合不是使用friend的结果,但是这些类之间的紧密耦合是由于它们的依赖关系,即他们定义的角色的后果。在这种情况下,friend是正确处理这种紧密耦合的机制。例如,容器及其对应的迭代器类在概念上紧密耦合。

在您的情况下,在我看来,这些类在概念上并没有那么紧密地耦合。您将friend用于不同的目的,即执行体系结构规则:只有Model才能使用方法LoadUpdateUse。不幸的是,这种模式有局限性:如果您有另一个类Foo和第二个体系结构规则,即只允许Foo调用Use方法,则不能同时表达两个体系结构规则:如果您使Foo也成为其他班级的朋友,那么Foo不仅将获得对Use的访问权限,而且还将获得对LoadUpdate的访问权限-无法细化授予访问权限。

如果我的理解是正确的,那么我会认为LoadUpdateUse在概念上不是private,也就是说,它们不代表应该为外部隐藏的类:它们属于该类的“官方” API,只有另外一条规则,即仅Model将使用它们。 private方法通常是私有的,因为实现者希望保留重命名或删除它们的自由,因为其他代码无法访问它们。我认为这不是这里的意图。

考虑到所有这些,我认为以不同的方式处理这种情况会更好。使方法LoadUpdateUse公开,并添加注释以解释体系结构约束。而且,尽管我的论点不是可测试性,但这也解决了您的测试问题之一,即允许您的测试也访问LoadUpdateUse

如果您还希望能够模拟TextureMaterialMesh类,那么请考虑Quarra的建议以介绍相应的接口。


尽管对于您的特定示例,我的建议是将方法LoadUpdateUse公开,但我不反对单元测试实现细节。同一接口的替代实现具有不同的潜在错误。而且,发现错误是测试的主要目标之一(请参阅Myers,Badgett,Sandler:软件测试的技巧,或Beizer:软件测试技术等)。

作为示例,考虑一下memcpy函数:假设您必须实现并测试它。您从一个简单的解决方案开始,逐字节复制,然后进行彻底测试。然后,您意识到对于32位计算机,如果源地址和目标地址是32位对齐的,则可以做得更快:在这种情况下,您可以一次复制四个字节。当您实施此更改时,新的memcpy内部看起来就很不一样:初步检查指针对齐是否合适。如果不合适,则执行原始的逐字节复制,否则执行更快的复制例程(还必须处理字节数不是四的倍数的情况,因此可能一些额外的字节要复制到最后)。

memcpy的界面仍然相同。不过,我认为您绝对需要扩展测试套件以实现新的实现:对于两个只有四个字节对齐的指针,对于只有一个指针是四个字节对齐的情况,您应该具有测试用例。指针都是四字节对齐的,并且要复制的字节数是四的倍数,并且它们不是四的倍数的情况,依此类推。也就是说,测试套件将得到极大的扩展-仅因为实现详细信息已更改。 需要新测试来查找新实现中的错误-尽管所有测试仍可以使用公共API,即memcpy函数。

因此,假设单元测试与实现细节无关,这是错误的,并且认为测试不是特定于实现的仅仅是错误的,因为它们是通过公共API测试的。

但是,正确的是,测试不应不必要依赖于实现细节。始终首先尝试创建与实现无关的有用测试,然后再添加特定于实现的测试。对于后者,测试私有方法(例如来自friend测试类)也可以是一个有效的选项-只要您知道其缺点(如果重命名,删除私有方法,则需要维护测试代码)等),并权衡优势。