让单元测试成为它正在测试的类的朋友有什么错?

在 C + + 中,我经常让一个单元测试类成为我正在测试的类的朋友。我这样做是因为有时候我觉得需要为一个私有方法编写一个单元测试,或者也许我想访问一些私有成员,这样我可以更容易地设置对象的状态,以便我可以测试它。对我来说,这有助于保存封装和抽象,因为我没有修改类的公共或受保护的接口。

如果我购买一个第三方库,我不希望它的公共接口被一堆我不需要知道的公共方法污染,仅仅因为供应商想要进行单元测试!

如果我从一个类继承,我也不想担心一大堆受保护的成员,我不需要知道这些成员。

这就是为什么我说它保留了抽象和封装。

在我的新工作中,他们甚至不赞成使用朋友类进行单元测试。他们说,因为类不应该“知道”关于测试的任何事情,而且您不希望类和它的测试之间有紧密耦合。

有没有人能给我多解释一下这些原因,好让我更好地理解?我只是不明白为什么使用朋友进行单元测试是不好的。

22979 次浏览

理想情况下,您根本不需要对私有方法进行单元测试。类的使用者应该关心的只是公共接口,因此应该对其进行测试。如果一个私有方法有一个 bug,它应该通过单元测试来捕获,该单元测试调用类上的一些公共方法,最终调用带有 bug 的私有方法。如果一个错误设法溜走了,这表明您的测试用例没有完全反映您希望您的类实现的契约。这个问题的解决方案几乎肯定是使用更详细的检查来测试公共方法,而不是让您的测试用例深入挖掘类的实现细节。

同样,这是理想的情况。在现实世界中,事情可能并不总是那么清楚,并且拥有一个单元测试类是它所测试的类的朋友,这可能是可以接受的,甚至是可取的。不过,这可能不是你想一直做的事情。如果它出现的频率足够高,那么这可能意味着您的类太大和/或执行的任务太多。如果是这样的话,通过将复杂的私有方法集重构为单独的类来进一步细分它们应该有助于消除单元测试了解实现细节的需要。

就像 bcat 建议的那样,您需要尽可能多地使用公共接口本身来查找 bug。但是,如果你想做一些事情,比如打印私有变量,并与预期的结果进行比较等(有助于开发人员轻松地调试问题) ,那么你可以让 UnitTest 类作为朋友的类进行测试。但是您可能需要将它添加到下面这样的宏下面。

class Myclass
{
#if defined(UNIT_TEST)
friend class UnitTest;
#endif
};

仅当需要单元测试时才启用 UNIT _ TEST 标志。 对于其他版本,您需要禁用此标志。

您应该考虑有不同的样式和方法要测试: 黑盒测试只测试公共接口(将类视为黑盒)。如果您有一个抽象基类,您甚至可以对所有的实现使用相同的测试。

如果使用 白盒测试,您甚至可以查看实现的细节。不仅包括类拥有哪些私有方法,还包括哪种条件语句(也就是说,如果您想增加条件覆盖率,因为您知道这些条件很难编码)。在白盒测试中,类/实现和测试之间肯定存在“高耦合”,这是必要的,因为您想要测试实现而不是接口。

正如 bcat 指出的,使用组合和更多但更小的类来代替许多私有方法通常是有帮助的。这简化了白盒测试,因为您可以更容易地指定测试用例以获得良好的测试覆盖率。

通常您只测试公共接口,以便可以自由地重新设计和重构实现。为私有成员添加测试用例定义了对类实现的需求和限制。

在许多情况下,我认为使用朋友单元测试类没有任何错误。是的,将一个大的类分解成更小的类有时是更好的方法。我认为人们有点太草率了,不会因为这样的事情而忽略使用好友关键字——这可能不是理想的面向对象设计,但是如果我真的需要的话,我可以牺牲一点理想主义来获得更好的测试覆盖率。

使要测试的函数受到保护。 现在在单元测试文件中,创建一个派生类。 创建公共包装函数,这些函数调用受测试类的 protected 函数。

我觉得 Bcat 给出了一个很好的答案,但是我想阐述一下他所提到的例外情况

在现实世界中,事情可能并不总是那么清楚 单元测试类是它所测试的类的朋友 可以接受的,甚至是可取的。

我工作的公司有一个很大的遗留代码库,它有两个问题,这两个问题都有助于使朋友的单元测试成为可取的。

  • 我们经常遇到需要重构的大型函数和类,但是为了重构,进行测试是很有帮助的。
  • 我们的大部分代码依赖于数据库访问,由于各种原因,不应该将数据库访问引入单元测试。

在某些情况下,Mocking 有助于缓解后一个问题,但通常这只会导致不必要的复杂设计(类层次结构在其他情况下不需要) ,而人们可以非常简单地用以下方式重构代码:

class Foo{
public:
some_db_accessing_method(){
// some line(s) of code with db dependance.


// a bunch of code which is the real meat of the function


// maybe a little more db access.
}
}

现在我们有了需要重构函数的情况,所以我们需要一个单元测试。不应该公开。现在,有一个很棒的技巧,叫做嘲笑,可以在这种情况下使用,但事实是,在这种情况下,嘲笑是过分的。这将需要我增加设计的复杂性与一个不必要的层次结构。

一个更加务实的方法是这样做:

 class Foo{
public:
some_db_accessing_method(){
// db code as before
unit_testable_meat(data_we_got_from_db);
// maybe more db code.
}
private:
unit_testable_meat(...);
}

后者为我提供了单元测试所需的所有好处,包括为我提供了宝贵的安全网,以捕捉在重构代码时产生的错误。为了对它进行单元测试,我必须加一个 UnitTest 类为好友,但是我强烈认为这比仅仅允许我使用 Mock 的无用代码层次结构要好得多。

我认为这应该成为一个习惯用法,并且我认为这是一个合适的、实用的解决方案,可以提高单元测试的 ROI。