替换功能单元

5
我正在为一个庞大的 Delphi 代码库编写单元测试基础设施。例如,我想将对SysUtils.FileExists的调用链接到“MockSysUtils.FileExists”。创建具有相同接口的SysUtils单元将不受编译器欢迎。我的想法是在运行时挂接我的模拟函数。现在是否可能?还有其他建议吗?祝好,Peter。
5个回答

11
运行时替换一个函数是很困难的,但通常在技术上是可能的。你需要做的是:
  • 获取要替换的函数地址
  • 反汇编前5个字节(检查是否有RET指令-非常小的程序可能会与另一个程序相邻,从而防止您替换它)
  • 更改它的页面保护 (使用VirtualProtect) 去可写状态
  • 使用JMP rel32指令(即E9<offset-to-your-func>)重写前5个字节
  • 像常规实现一样实现您版本的函数,确保它与您模拟的函数具有相同的参数和调用约定

更简单的方法是链接到不同版本的SysUtils.pas。这将需要您重新编译依赖于SysUtils.pas的RTL和VCL中的所有单元,但它可能比上面描述的函数插桩方法容易得多。

最简单的方法是在语言级别上操作,其中您直接不依赖于SysUtils(因此可以在更高的级别上切换),或者修改uses声明以有条件地引用不同的单元。


“或者您可以修改使用声明,有条件地引用不同的单元。” - 我认为这对于此来说是最容易的。确保您的特殊单元是最后一个,然后您就可以重新定义任何函数。 - mj2008
它假设单元源可用。如果是这样,那么这很容易;然而,提问者确实询问了运行时! - Barry Kelly

7
你可以使用 MadCodeHook 来实现。使用 HookCode 函数,给它想要替换的函数地址和想要调用的函数地址。它会返回一个函数指针,你可以用它来调用原始函数并在取消挂钩后使用。实质上,它实现了 Barry 描述中的中间三个步骤。
我认为个人使用的 MadCodeHook 是免费的。如果你想找比这更自由的东西,可以尝试找到 Tnt Unicode Controls 的旧版本。它使用相同的钩子技术将 Unicode 支持注入到一些 VCL 代码中。你需要一个旧版本,因为更新的版本不再免费。在 TntSystem.pas 中找到 OverwriteProcedure 函数,那里也有如何使用它的示例。
代码挂钩很好,因为它不需要重新编译RTL和VCL,并且不涉及条件编译来控制范围内的哪些函数。您可以从单元测试设置过程中挂载代码,原始代码永远不会知道差异。它会认为自己在调用原始的FileExists函数(因为实际上确实是这样),但当它到达那里时,它将立即跳转到您的模拟版本。

1

你也可以将仅包含你想要模拟的函数的单元添加到测试单元的 uses 子句中。Delphi 将始终使用列在最后的单元中的函数。不幸的是,这将需要你更改要测试的单元。

你的 Mock-Sysutils 单元:

unit MockSysutils;

interface

function FileExists(...) ...
...
end.

你想要测试的单元:

unit UnitTotest;

interface

uses
  Sysutils,
  MockSysUtils;

...

  if FileExists(...) then

FileExists现在将调用MockSysutils版本而不是Sysutils版本。


0

谢谢,

如果有TSysUtils类的话,我可以通过继承我的MockSysUtils来使用它,但是现在并没有这样的情况,而且代码库非常庞大。虽然会逐步替换,但我想知道是否有快速解决方案。

第一种方法对于一个函数可能还可以,但在这种情况下不太可行。

我将采用第二种方法。


0

这可能有一点儿超前,但是这里还有另一个选择。

在构建单元测试和主代码库时,您可以使用grep查找所有要替换的函数,并指定要使用的单元。

而不是

fileexists(MyFilename);

你可以使用 grep fileexists 命令来查找并替换

MockTests.fileexists(MyFileName);

如果您在构建时(使用自动化构建工具)执行此操作,它将很容易完成,并为您提供最大的灵活性。您可以简单地拥有一个配置文件,列出所有要替换的函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接