【问题标题】:C++ Unit Testing Legacy Code: How to handle #include?C++ 单元测试遗留代码:如何处理#include?
【发布时间】:2010-09-09 01:38:05
【问题描述】:

我刚刚开始使用#include 指令为具有大量物理依赖性的遗留代码模块编写单元测试。我一直在用一些感觉过于乏味的方法来处理它们(提供空标题来打破长#include依赖列表,并使用#define来防止类被编译)并且正在寻找一些更好的策略来处理这些问题。

我经常遇到这样的问题,即用空白版本复制几乎每个头文件,以便将我正在测试的整个类分开,然后为对象编写大量存根/模拟/假代码,这些代码将需要替换,因为它们现在未定义。

有人知道一些更好的做法吗?

【问题讨论】:

  • 我希望我能把这个投票赞成 x 10,这是迄今为止我读过的关于 SO 的最佳问题。我已经记不清有多少人会放弃“我们无法测试..我们有 C++ 代码..嘘!”
  • Gishu,完全同意。我在这里工作的地方也有同样的讨论。

标签: c++ unit-testing legacy


【解决方案1】:

回复中的沮丧情绪是压倒性的......但不要害怕,我们有the holy book to exorcise the demons of legacy C++ code。如果您已经排了一个多星期的时间与遗留 C++ 代码较量,请认真购买这本书。

转到第 127 页:可怕的包含依赖项的情况。(现在我什至离 Michael Feathers 几英里远,但我可以尽可能短地回答这个问题。。 )

问题:在 C++ 中,如果 classA 需要了解 ClassB,则 Class B 的声明是直接提升的/以文本形式包含在 ClassA 的源文件中。而且由于我们的程序员喜欢把它带到错误的极端,一个文件可以递归地包含无数其他的可传递文件。构建需要数年......但至少它会构建......我们可以等待。

现在说“在测试工具下实例化 ClassA 很困难”是轻描淡写的。 (引用 MF 的示例 - 调度程序是我们的海报问题孩子,拥有大量的依赖。)

#include "TestHarness.h"
#include "Scheduler.h"
TEST(create, Scheduler)     // your fave C++ test framework macro
{
  Scheduler scheduler("fred");
}

这将带出一连串构建错误的包含龙。
Blow#1 Patience-n-Persistence:一次处理每个包含一个,然后决定我们是否真的需要那个依赖。假设 SchedulerDisplay 是其中之一,其 displayEntry 方法在 Scheduler 的 ctor 中调用。
Blow#2 Fake-it-till-you-make-it(感谢 RonJ):

#include "TestHarness.h"
#include "Scheduler.h"
void SchedulerDisplay::displayEntry(const string& entryDescription) {}
TEST(create, Scheduler)
{
  Scheduler scheduler("fred");
}

然后 pop 去依赖和它所有的传递包含。 您还可以通过将 Fake 方法封装在 Fakes.h 文件中以包含在您的测试文件中来重用 Fake 方法。
Blow#3 练习:它可能并不总是那么简单.. 但你明白了。在前几场决斗之后,破坏部门的过程将变得容易-n-机械

注意事项(我有提到有注意事项吗?:)

  • 我们需要为这个文件中的测试用例单独构建;我们在一个程序中只能有一个 SchedulerDisplay::displayEntry 方法的定义。因此,为调度程序测试创建一个单独的程序。
  • 我们不会破坏程序中的任何依赖项,因此我们不会使代码更简洁。
  • 只要我们需要测试,您就需要维护这些假货。
  • 你的审美可能会被冒犯一段时间..咬紧嘴唇,“和我们一起,共创美好明天”

将此技术用于具有严重依赖性问题的非常大的类。不要经常或轻率地使用.. 将此作为深入重构的起点。随着时间的推移,当您提取更多类(通过他们自己的测试)时,这个测试程序可以在谷仓后面进行。

更多..请阅读本书。无价。兄弟加油!

【讨论】:

  • 虽然我发现这是一个可以接受的答案,但我觉得它确实掩盖了在替代函数实现中提供假存根和在构建过程中必须执行的魔法之间的过程。
  • 图书链接已损坏。我认为它指向 Michael Feathers 的“有效地使用遗留代码”。
【解决方案2】:

由于您正在测试遗留代码,我假设您无法重构所述代码以减少依赖项(例如,通过使用 pimpl idiom

恐怕这会让你别无选择。每个类型或函数包含的标头都需要该类型或函数的模拟对象才能编译所有内容,您无能为力...

【讨论】:

    【解决方案3】:

    我没有直接回答你的问题,但我担心如果你使用大量遗留代码,单元测试可能不是要做的事情。

    在带领 XP 团队完成一个全新的开发项目后,我真的很喜欢我的单元测试。事情发生了,几年后我发现自己正在处理一个存在很多质量问题的大型遗留代码库。

    我试图找到一种将单元测试添加到应用程序的方法,但最终卡在了一个 catch-22 中:

    1. 为了编写有意义的完整单元测试,需要重构代码。
    2. 没有单元测试,重构代码太危险了。

    如果您觉得自己像个英雄,并且在单元测试中喝了凉水,那么您仍然可以尝试一下,但存在真正的风险,即您最终会得到更多价值不大的测试代码,而现在也需要编写这些代码维护。

    有时最好以“设计”的方式处理代码。

    【讨论】:

      【解决方案4】:

      我不知道这是否适用于您的项目,但是 您可以尝试从构建的链接阶段解决问题。

      这将完全消除您的#include 问题。 您需要做的就是重新实现包含文件中的接口以执行您想要的任何操作,然后只需链接到您创建的模拟对象文件以实现包含文件中的接口。

      这种方法的最大缺点是构建系统更复杂。

      【讨论】:

        【解决方案5】:

        如果您继续编写存根/模拟/伪造代码,您可能会对具有不同行为的类进行单元测试,然后在主项目上编译。

        但如果这些包含存在并且没有添加行为,那么没关系。

        在进行单元测试时,我会尽量不更改包含的任何内容,这样您就可以确定(就您可以使用遗留代码而言:))您正在测试真正的代码。

        【讨论】:

          【解决方案6】:

          对于具有大量依赖项的遗留代码,您肯定处于困境和困境之间。要解决所有问题,您还有很长的路要走。

          从您所说的来看,您似乎正试图依次保持每个模块的源代码完整,将其放置在模拟外部依赖项的测试工具中。我的建议是采取更勇敢的步骤,尝试进行一些重构以消除(或invert)依赖项,这可能正是您要避免的步骤。

          我建议这样做是因为我猜测依赖项会在你编写测试时杀死你。如果你能消除依赖关系,从长远来看,你肯定会更好。

          【讨论】:

            猜你喜欢
            • 2014-03-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-12-05
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多