C++ 单元测试遗留代码:如何处理#include?
我刚刚开始使用 #include 指令为具有大量物理依赖性的遗留代码模块编写单元测试。 我一直在用一些感觉过于乏味的方法来处理它们(提供空头来打破长#include依赖项列表,并使用#define来防止类被编译),并且正在寻找一些更好的策略来处理这些问题。
我经常遇到这样的问题:用空白版本复制几乎每个头文件,以便将我正在测试的类完整地分开,然后为需要的对象编写大量的存根/模拟/假代码。被替换,因为它们现在未定义。
有人知道一些更好的做法吗?
I've just started writing unit tests for a legacy code module with large physical dependencies using the #include directive. I've been dealing with them a few ways that felt overly tedious (providing empty headers to break long #include dependency lists, and using #define to prevent classes from being compiled) and was looking for some better strategies for handling these problems.
I've been frequently running into the problem of duplicating almost every header file with a blank version in order to separate the class I'm testing in it's entirety, and then writing substantial stub/mock/fake code for objects that will need to be replaced since they're now undefined.
Anyone know some better practices?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
由于您正在测试遗留代码,我假设您无法重构所述代码以减少依赖性(例如,通过使用 pimpl 习语)
恐怕这让你几乎没有选择。 类型或函数包含的每个标头都需要该类型或函数的模拟对象才能编译所有内容,您无能为力......
Since you're testing legacy code I'm assuming you can't refactor said code to have less dependencies (e.g. by using the pimpl idiom)
That leaves you with little options I'm afraid. Every header that was included for a type or function will need a mock object for that type or function for everything to compile, there's little you can do...
回复中的抑郁情绪是压倒性的......但别担心,我们有驱除遗留 C++ 代码恶魔的圣书。 如果您已经与遗留的 C++ 代码进行了长达一周的较量,那么真的请购买这本书。
翻到第 127 页:可怕的包含依赖性的情况。(现在我距离 Michael Feathers 还不到几英里,但这里是我可以管理的答案......)
问题:在 C++ 中,如果 classA 需要了解 ClassB,则 Class B 的声明将直接提升/文本包含在 ClassA 的源文件中。 由于我们程序员喜欢将其带到错误的极端,因此一个文件可以递归地包含无数个其他文件。 构建需要数年时间..但至少它构建起来..我们可以等待。
现在说“在测试工具下实例化 ClassA 很困难”是轻描淡写的说法。 (引用 MF 的示例 - Scheduler 是我们的海报问题子,具有大量的 deps。)
这将带来包含龙以及一系列构建错误。
Blow#1 Patience-n-Persistence:每次包含一个,然后决定我们是否真的需要这种依赖。 假设 SchedulerDisplay 是其中之一,它的 displayEntry 方法在 Scheduler 的 ctor 中调用。
Blow#2 Fake-it-till-you-make-it(感谢 RonJ):
pop 消除了依赖项及其所有传递性包含。
您还可以通过将 Fakes 方法封装在要包含在测试文件中的 Fakes.h 文件中来重用 Fake 方法。
吹#3练习:它可能并不总是那么简单..但你明白了。 在最初的几次决斗之后,打破部门的过程将变得简单机械
警告(我有没有提到有警告?:)
将此技术用于具有严重依赖性问题的非常大的类。 不要经常或轻易使用。使用它作为更深入重构的起点。随着时间的推移,当您提取更多类(带有它们自己的测试)时,这个测试程序可以在谷仓后面进行。
欲了解更多..请阅读本书。 无价。 战斗吧兄弟!
The depression in the responses is overwhelming... But don't fear, we've got the holy book to exorcise the demons of legacy C++ code. Seriously just buy the book if you are in line for more than a week of jousting with legacy C++ code.
Turn to page 127: The case of the horrible include dependencies. (Now I am not even within miles of Michael Feathers but here as-short-as-I-could-manage answer..)
Problem: In C++ if a classA needs to know about ClassB, Class B's declaration is straight-lifted / textually included in the ClassA's source file. And since we programmers love to take it to the wrong extreme, a file can recursively include a zillion others transitively. Builds take years.. but hey atleast it builds.. we can wait.
Now to say 'instantiating ClassA under a test harness is difficult' is an understatement. (Quoting MF's example - Scheduler is our poster problem child with deps galore.)
This will bring out the includes dragon with a flurry of build errors.
Blow#1 Patience-n-Persistence: Take on each include one at a time and decide if we really need that dependency. Let's assume SchedulerDisplay is one of them, whose displayEntry method is called in Scheduler's ctor.
Blow#2 Fake-it-till-you-make-it (Thanks RonJ):
And pop goes the dependency and all its transitive includes.
You can also reuse the Fake methods by encapsulating it in a Fakes.h file to be included in your test files.
Blow#3 Practice: It may not be always that simple.. but you get the idea. After the first few duels, the process of breaking deps will get easy-n-mechanical
Caveats (Did I mention there are caveats? :)
Use this technique for a very huge class with severe dependency issues. Don't use often or lightly.. Use this as a starting point for deeper refactorings. Over time this testing program can be taken behind the barn as you extract more classes (WITH their own tests).
For more.. please do read the book. Invaluable. Fight on bro!
我不会直接回答您的问题,但我担心如果您使用大量遗留代码,单元测试可能不适合做。
在领导 XP 团队完成一个新开发项目后,我真的很喜欢我的单元测试。 事情发生了,几年后,我发现自己正在处理一个存在很多质量问题的大型遗留代码库。
我试图找到一种向应用程序添加单元测试的方法,但最终陷入了第 22 条军规:
如果您觉得自己像个英雄,并且在单元测试中喝了一杯清凉饮料,那么您仍然可以尝试一下,但存在真正的风险,即您最终会得到更多现在也需要维护的几乎没有价值的测试代码。
有时,最好以“设计”的方式处理代码。
I am not answering your question directly but I am afraid that unit testing just may not be the thing to do if you work with large amounts of legacy code.
After leading an XP team on a green field development project I really loved my Unit tests. Things happened and a few years later I find myself working on a large legacy code base that has lots of quality problems.
I tried to find a way to add units tests to the application but in the end just got stuck in a catch-22:
If you feel like a hero and drink the cool-aid on unit testing then you may still give it a try but there is a real risk that you end up with just more test code of little value that now also needs to be maintained.
Sometimes it is just best to work on the code in the way that is "designed" to be worked on.
如果您继续编写存根/模拟/假代码,您可能会冒着对具有与在主项目上编译时不同行为的类进行单元测试的风险。
但如果这些包含存在并且没有添加行为,那么就可以了。
在进行单元测试时,我会尝试不更改包含内容中的任何内容,这样您就可以确定(就遗留代码而言:))您测试的是真实代码。
If you keep writing stubs/mock/fake codes you risk doing unit testing on a class that has different behavior then when compiled on the main project.
But if those includes are there and have no added behavior then it's Ok.
I'd try not changing anything on the includes while doing the unit testing so you're sure (as far you can be on legacy code :) ) that you testing the real code.
我不知道这是否适用于您的项目,但是
您可能会尝试从构建的链接阶段解决问题。
这将完全消除您的#include 问题。
您需要做的就是重新实现包含文件中的接口来执行您想要的操作,然后链接到您创建的模拟对象文件以实现包含文件中的接口。
这种方法的一大缺点是构建系统更加复杂。
I don't know if this will work for your project but
you might try to attack the problem from the link phase of your build.
This would completely eliminate your #include problem.
All you would need to do is re-implement the interfaces in the included files to do what ever you want and then just link to the mock object files that you have created to implement the interfaces in the include file.
The big disadvantage to this method is a more complected build system.
对于具有大量依赖项的遗留代码,您肯定陷入进退两难的境地。 要解决这一切,你还有很长的路要走。
从您所说的来看,您似乎试图依次保持每个模块的源代码完整,将其放置在模拟了外部依赖项的测试工具中。 我的建议是采取更勇敢的步骤,尝试进行一些重构来消除(或 反转)依赖关系,这可能正是您想要避免的步骤。
我建议这样做是因为我猜测当你编写测试时依赖项会杀死你。 如果您能够消除依赖性,从长远来看,您肯定会过得更好。
You're definitely between a rock and a hard place with legacy code with large dependencies. You've got a long hard slog ahead to sort it all out.
From what you say, it seems you are trying to keep the source code intact for each module in turn, placing it in a test harness with external dependencies mocked out. My suggestion here would be to take the even braver step of attempting some refactoring to eliminate (or invert) the dependencies, which is probably the very step you are trying to avoid.
I suggest this because I'm guessing the dependencies are going to kill you as you write tests. You will certainly be better off in the long term if you can eliminate the dependencies.