What is wrong with making a unit test a friend of the class it is testing?
在C++中,我经常让一个单元测试类成为我正在测试的类的朋友。我这样做是因为有时我觉得需要为私有方法编写一个单元测试,或者可能我想要访问一些私有成员,这样我可以更容易地设置对象的状态,以便测试它。对我来说,这有助于保留封装和抽象,因为我没有修改类的公共接口或受保护接口。
如果我买了第三方库,我不希望它的公共接口被一堆我不需要知道的公共方法污染,仅仅因为厂商想要单元测试!
我也不想担心一堆受保护的成员,如果我是从一个类继承的,我不需要知道这些成员。
这就是为什么我说它保留了抽象和封装。
在我的新工作中,他们甚至不赞成使用朋友类进行单元测试。他们这样说是因为类不应该"知道"关于测试的任何内容,并且您不希望类与其测试紧密耦合。
有人能多给我解释一下这些原因,以便我能更好地理解吗?我只是不明白为什么使用朋友进行单元测试是不好的。
理想情况下,您根本不需要对私有方法进行单元测试。类的所有使用者都应该关心的是公共接口,所以这是您应该测试的。如果一个私有方法有一个bug,它应该被一个在类上调用某个公共方法的单元测试捕获,该方法最终会调用bugy私有方法。如果一个bug成功地溜了过去,这表明您的测试用例没有完全反映您希望类实现的契约。这个问题的解决方案几乎可以肯定是用更仔细的方式测试公共方法,而不是让您的测试用例深入到类的实现细节中。
同样,这是理想情况。在现实世界中,事情可能并不总是那么清楚,让一个单元测试类成为它测试的类的朋友可能是可以接受的,甚至是可取的。不过,这可能不是你一直想做的事情。如果它看起来经常出现,那么这可能意味着你的类太大和/或执行了太多的任务。如果是这样,通过将复杂的私有方法集合重构为单独的类来进一步细分它们,应该有助于消除单元测试了解实现细节的需要。
您应该考虑到要测试的样式和方法不同:黑盒测试只测试公共接口(将类视为黑盒)。如果您有一个抽象基类,甚至可以对所有实现使用相同的测试。
如果您使用白盒测试,您甚至可以查看实现的细节。不仅要知道一个类有哪些私有方法,还要包括什么类型的条件语句(例如,如果您想增加条件覆盖率,因为您知道条件很难编码)。在白盒测试中,类/实现与测试之间肯定存在"高度耦合",这是必要的,因为您希望测试实现而不是接口。
正如bcat指出的那样,使用组合和更多但更小的类,而不是许多私有方法,通常会有所帮助。这简化了白盒测试,因为您可以更容易地指定测试用例以获得良好的测试覆盖率。
我觉得BCAT给出了一个很好的答案,但我想详细说明他提到的例外情况。
In the real world, things may not always be so clear, and having a
unit testing class be a friend of the class it tests might be
acceptable, or even desirable.
我在一家拥有大量遗留代码库的公司工作,这家公司有两个问题,这两个问题都有助于让朋友进行单元测试。
- 我们遇到了非常大的函数和类,它们需要重构,但是为了重构,进行测试是很有帮助的。
- 我们的大部分代码都依赖于数据库访问,出于各种原因,不应该将其引入单元测试中。
在某些情况下,模拟有助于缓解后一个问题,但这通常只会导致不成功的复杂设计(类继承,否则不需要),而人们可以非常简单地按照以下方式重构代码:
1 2 3 4 5 6 7 8 9 10 | 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. } } |
现在我们有了这样的情况:函数的核心部分需要重构,所以我们需要一个单元测试。它不应该公开曝光。现在,有一个奇妙的技术,他称之为模拟,可以在这种情况下使用,但事实是,在这种情况下,模拟是多余的。它将要求我增加复杂的设计与一个不必要的继承权。
一个更务实的方法是这样做:
1 2 3 4 5 6 7 8 9 10 | 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(...); } |
后者为我提供了单元测试所需的所有好处,包括为我提供了宝贵的安全网,以捕获我在肉中重构代码时产生的错误。为了对它进行单元测试,我必须和一个单元测试类交朋友,但是我强烈地认为这比一个无用的代码继承性要好得多,只是为了让我使用一个模拟。
我认为这应该成为一个习语,我认为这是一个合适的、实用的解决方案,可以提高单元测试的投资回报率。
正如bcat建议的那样,您需要尽可能地使用公共接口本身来查找错误。但是,如果您想做一些事情,比如打印私有变量、与预期结果比较等(有助于开发人员轻松调试问题),那么您可以让UnitTest类成为待测试类的朋友。但您可能需要在下面这样的宏下添加它。
1 2 3 4 5 6 | class Myclass { #if defined(UNIT_TEST) friend class UnitTest; #endif }; |
仅当需要单元测试时才启用标记单元测试。对于其他版本,您需要禁用此标志。
在很多情况下,我认为使用朋友的单元测试类没有任何问题。是的,将一个大类分解成小类有时是更好的方法。我认为人们对于使用friend关键字进行类似的事情有点过于仓促,以至于不能拒绝使用friend关键字-这可能不是理想的面向对象的设计,但是如果这是我真正需要的,我可以牺牲一点理想主义来获得更好的测试覆盖率。
通常,您只测试公共接口,以便可以自由地重新设计和重构实现。为私有成员添加测试用例定义了对类实现的要求和限制。
使要测试的函数受保护。现在在单元测试文件中,创建一个派生类。创建在受测试保护的函数下调用类的公共包装函数。