首先,是一个演示。以下是仅使用6行Makefile[(取决于您如何计算它。)]实现的结果:
⚠️这里应该有一个终端录制播放器,但是看起来JavaScript还在加载,或者您已经阻止了第一方JavaScript。如果要在不使用JavaScript的情况下播放录音,可以在任何支持ttyrec格式的软件中播放。我使用Termrec,下面将使用它来播放:
让我们直接跳到一个示例Makefile,通常使用文件名Makefile:
r通配符=$(foreach d,$(通配符$1*),$(调用r通配符,$d/,$2)$(筛选器$2,$d))。PHONY:测试测试:$(patsubst%.test,%.stdout,$(调用r通配符,%.test))%.stdout:%.test./$<;>;$@2>;$(patsubst%.stdout,%.。false)git diff--退出代码--src-prefix=预期/--dst-prefix=Actual/\$@$(patsubst%.stdout,%.stderr,$@)\||(touch--date=@0$@;false)。
如果你熟悉makefile,这可能足以让你理解整篇文章,你就可以停止阅读了。这篇文章的其余部分解释了如何使用它,它是如何工作的,以及该方法的局限性。
请注意,这都是针对GNU make的。其他make实现可能不支持所有foreach、通配符、调用、过滤器和patsubst函数,因此您可能需要以另一种方式写出测试目标的先决条件。然而,总体原则是合理的。
创建一个可执行文件[也就是说,在Unixy平台上,它需要使用mode+x,并且要么是可执行二进制文件,要么以Shebang开头。]。名为foo.test,它包含要测试的脚本。
运行make test(测试运行似乎会成功,因为预期的输出还没有添加到Git中[Git中的大多数内容忽略未跟踪的文件;Git Diff…。stderr不输出任何内容,并在未跟踪这两个文件时报告成功。])。
检查刚刚创建的foo.stdout和foo.stderr的内容,确保它们符合您的预期。
这与Git没有本质上的联系;您可以将git diff调用替换为来自另一个版本控制系统的语义等价的调用。
正如您希望看到的,这很简单,只有六行。然而,它的功能非常强大:通过使用make,您可以免费获得各种美好的魔法。
如果测试在其他输入(例如,测试数据或构建构件)上有所不同,您只需向目标添加新的前提条件:
如果你愿意,你可以对前提条件有更多的了解。这些功能允许您只运行输入(无论是数据还是代码)已更改的测试,而不是所有测试。
如果您始终希望运行所有测试(如果您尚未设置精确的依赖项跟踪,则可能希望这样做),请标记所有%.stdout目标假(将在下面解释):
如果您只想运行所有测试一次,那么运行make--ways-make test[--ways-make的缩写是-B,但我建议在大多数地方使用长选项。]。
如果您想并发运行测试,可以使用make--Jobs=8test[缩写形式-j8,这是我确实使用缩写形式的测试。]。或者类似的。(注意:与--KEEP-GOGING结合使用,可能会将来自多个Diffs的行交错输出到终端。)
默认情况下,它将在第一次测试失败时退出,但是--继续[缩写-k。]。将继续运行所有测试,直到尽其所能地完成为止。(如果您知道所做的更改将更改许多测试的输出,因此它将同时更新它们的所有stdout和stderr文件,则这几乎是必要的。)。
让我们一点一点地将这六行分开,看看它是如何工作的。
此r通配符函数是递归通配符匹配器。$(通配符)执行单一级别的匹配;这会使其达到多个级别。
我不打算解释它是如何工作的所有细节,但以下是它实现的算法的近似值,用伪代码表示:
函数r通配符(目录,模式):对于每个项目,作为目录中的路径:如果路径是目录:发出所有r通配符(路径,模式),否则(路径是文件):如果路径匹配模式:发出路径。
我使用$(调用r通配符,%.test)而不是$(Shell Find)。-name\*.test)主要是为了兼容性,因此不需要查找二进制文件(GNU findutils或其他变体)。
PHONY:test规则将test标记为假目标,这在大多数情况下不是必需的,但是会稍微加快速度,并确保在创建名为“test”的文件时测试不会中断。如果您将单个测试标记为虚假,结果是它不会检查文件修改时间,而只会始终重新运行测试。
然后是针对实际测试目标的规则。它没有配方,这意味着当您运行make test时,在它运行所有必要的测试之后,不会发生任何额外的事情:它只是各个部分的总和,没有更多。
它的部件?它依赖于$(patsubst%.test,%.stdout,$(调用r通配符,%.test)),这意味着“递归查找所有*.test文件($(调用r通配符,%.test)),然后将每个文件的‘.test’扩展名更改为‘.stdout’($(patsubst%.test,%.stdout,…))”。
您可能想知道为什么它需要依赖于foo.stdout而不是foo.test:这是因为foo.stdout是将运行测试的目标,而依赖于foo.test只能确保测试的存在。
您可能想知道为什么我们搜索foo.test定义文件并将其扩展名更改为.stdout,而不仅仅是搜索.stdout文件:这样我们就可以创建新的测试,而不需要手动创建.stdout文件。
这样做的结果是,我们创建了一个名为“test”的假目标,它取决于所有测试的结果。说它运行所有测试是不正确的;测试目标并不运行测试,而是声明“我要求在运行我之前运行测试”[请考虑如何恰当地将这些事情命名为先决条件而不是依赖项。]。(正如所讨论的,运行测试目标本身不会做任何事情,因为它没有配方)。每次运行所有测试的方法是将它们全部标记为false;否则,make将查看它们的先决条件树,并可能观察到它们已经满足要求,其中stdout文件比测试文件更新,因此会说“该测试已经运行,不需要再次运行”。
%.stdout:%.test./$<;>;$@2>;$(patsubst%.stdout,%.stderr,$@)\||(touch--date=@0$@;false)git diff--exit-code--src-prefix=Expect/-dst-prefix=Actual/\$@$(patsubst%.stdout,%.stderr,$@)\||(。
我们使用隐式规则,这样就不需要枚举所有文件(我们可以通过几种方式做到这一点,但这会更痛苦)。
此规则规定“通过运行这两个命令,可以基于具有相同基本名称(但扩展名为‘.test’)的文件创建扩展名为‘.stdout’的任何文件”。
$<;扩展为第一个先决条件的名称,在本例中为foo.test。
(行尾的反斜杠是行续行符,这一点也很重要。配方的每一行都是单独调用的。[您可以使用.ONESHELL:选择退出此行为,但这是在GNU to make 3.82中引入的,而且MacOS出于许可原因包括了古老的GNU to make 3.81,所以在使用它之前要考虑兼容性。])。
./foo.test>;foo.stdout 2>;foo.stderr\||(touch--date=@0 foo.stdout;false)git diff--exit-code--src-prefix=预期/--dst-prefix=Actual/\foo.stdout foo.stderr\||(touch--date=@0 foo.stdout;false)。
如果失败:zerfoo.stdout的mtime[即,将文件的“上次修改”时间更新为Unix纪元1970-01-01T00:00:00Z。]。因此它比foo.test旧(因此,make的后续调用不会认为foo.stdout目标是满足的;您也可以删除该文件,但这样做用处较小),然后失败(这将导致make停止执行配方,并报告失败)。
运行git diff,打印对这些文件的任何未暂存的更改(即:测试输出与预期不符的任何方式)。
如果对这些文件有任何未暂存的更改,则将foo.stdout的mtime清零(原因与前面相同)并报告失败。
如前所述,假设每次运行命令的输出都是相同的,我们只是在做一个天真的差异。
在实践中,通常会有一些细微的变化区域,例如时间戳或持续时间数字。
例如,如果您有生成带有?t=时间戳或?的URL的东西。散列缓存破坏,您可能希望将时间戳置零或将散列转换为常量值。如果您的测试文件是shell脚本,则清理输出可能如下所示:
这种通用的方法允许您丢弃随机性来源,甚至量化它(例如,丢弃毫秒,但保留秒),但是很难做任何更花哨的事情,比如通过数字比较来检查某个值是否在给定的误差范围内-如果您不小心,就会开始编写一个测试框架,而不是使用git diff作为测试框架。[如果您真的想在这里走得更远,那就开始考虑如何应用Git筛选器属性。但我建议不要这样做,即使这是可能的!]。
在当前的语言和环境中,正确声明依赖项通常是不可行的,因此您最终可能需要过于宽泛的先决条件,如“此测试依赖于所有源文件”,因此可能会运行比实际需要更多的测试。
在各地启动流程可能会很昂贵。解释器通常需要数百毫秒才能启动,更不用说需要多长时间来导入代码了,您现在每次测试都需要做一次,而不是整个测试一次。
对输出进行过滤以使其具有确定性,从而有效地限制您进行相等性检查,而不是比较。在很大程度上,这是一个测试框架,只有断言,几乎没有逻辑。
Git不以任何方式管理文件修改时间。一旦你提交了本质上是构建工件的东西,这可能会成为一件麻烦事,因为make是根据mtime做出决定的。只要正确指定了所有的依赖项,应该不会造成任何破坏[在一般情况下,这实际上并不完全正确;几个月前我在工作中遇到过一个案例,我在那里临时向存储库添加了一个构建构件,它通常工作得很好,但只是偶尔mtime会出现在前面,构建服务器会尝试重新构建它并失败,因为它的网络访问被锁定了,但是构建那个特定的构件需要访问Internet。]。如果您不使用Git修改stdout文件,但这可能会导致不必要的测试运行。当你想稳妥行事时,Make--Always-Make测试将是你的朋友。
当您在提供测试工具的生态系统中时,您可能应该使用它;但是在这样的生态系统之外,shell脚本、生成文件和版本控制的功能确实可以很好地工作,以最小的努力产生良好的结果。
这种方法有各种限制,但对于许多事情来说,它工作得非常好,并且可以扩展很多。我特别喜欢它跟踪预期输出的方式,使手动检查变得简单,更新变得微不足道。
我以前使用过类似的方法,我发现makefile在大大小小的各种事情上通常都非常有效;我认为这种技术展示了make的一些巧妙功能。
GNU make文档相当不错。有时很难在索引中找到您要查找的内容,但信息肯定都在那里。
评论?问题?改过自新?如果你想就这篇文章中的任何事情与我联系,请发电子邮件给我,邮箱是[email protected]。