是否有原因未将测试与其测试代码内联地编写?


91

最近,我一直在阅读一些有关Literate Programming的文章,这让我开始思考...写得井井有条的测试(尤其是BDD风格的规范)在解释代码作用方面比散文效果更好,并且具有以下优点:验证自己的准确性。

我从未见过将测试与其代码内联地编写的测试。这是仅是因为语言在编写到相同的源文件中时不会趋向于将应用程序和测试代码分开(而没有人使之变得容易),还是人们在原则上将测试代码与应用程序代码分开呢?


33
像python和doctest这样的编程语言可以使您做到这一点。
西蒙·贝格

2
您可能会觉得BDD风格的规范在解释代码方面比散文要好,但这并不意味着两者的结合并不是更好。
JeffO

5
这里一半的参数也适用于内联文档。
CodesInChaos

3
@Simon doctest对于过于严格的测试来说过于简单了,主要是因为它们不是为此而设计的。它们旨在并擅长在文档中提供可以自动验证的代码示例。现在,有些人也将它们用于单元测试,但是最近(就像过去几年一样)花了很多时间,因为它往往以易碎的混乱,过于冗长的“文档”和其他混乱为结尾。

7
按合同设计允许使用内联规范,使测试变得简单。
Fuhrmanator

Answers:


89

我可以想到的用于内联测试的唯一优势是减少要写入的文件数量。对于现代IDE,这并不是什么大问题。

但是,内联测试有许多明显的缺点:

  • 它违反了关注点分离。这可能值得商,,但对我而言,测试功能与实现功能是不同的责任。
  • 您可能不得不引入新的语言功能来区分测试/实现,或者冒着模糊两者之间界限的风险。
  • 较大的源文件更难使用:更难阅读,更难理解,您更有可能必须处理源代码控制冲突。
  • 可以这么说,我认为戴上“测试人员”的帽子会变得更加困难。如果您正在查看实施细节,您将更倾向于跳过实施某些测试。

9
那很有意思。我想我能看到的好处是,当您戴上“编码器”帽子时,您想考虑一下测试,但这是正确的,但事实并非如此。
克里斯·德沃罗

2
按照这些思路,有可能(也许是合乎需要的)让一个人创建测试,然后由另一个人实际执行代码。在线进行测试会使这变得更加困难。
Jim Nutt

6
如果可以的话,我会拒绝投票。无论如何,这是一个怎样的答案?实施者不编写测试吗?人们是否在查看实施细节时跳过测试?“太难了”在大文件上发生冲突??以及如何将测试与实现细节相混淆???
bharal

5
@bharal另外,对“只是太难了”,受虐狂是傻瓜的美德。除了我实际上要解决的问题之外,我希望一切都变得简单。
deworde

3
单元测试可以视为文档。这表明出于与注释相同的原因,应在代码中包含单元测试,以提高可读性。但是,这样做的问题是往往会有大量的单元测试,并且有大量的测试实现开销没有指定预期的结果。甚至代码中的注释也应保持简洁,将较大的解释移开-移至函数外部的注释块,单独的文件或设计文档中。IMO的单元测试很少,即使足够短,也无法保留在注释之类的测试代码中。
Steve314

36

我可以想到一些:

  • 可读性。散布“真实”代码和测试将使阅读真实代码更加困难。

  • 代码膨胀。将“真实”代码和测试代码混合到相同的文件/类/可能会导致更大的编译文件的位置,等等。这对于后期绑定的语言尤其重要。

  • 您可能不希望您的客户看到您的测试代码。(我不喜欢这个原因……但是,如果您正在开发一个封闭源代码项目,那么无论如何测试代码都不太可能对客户有所帮助。)

现在,每个问题都有可能的解决方法。但是,海事组织(IMO),首先不去那里比较容易。


值得一提的是,在早期,Java程序员曾经做过这种事情。例如main(...),在类中包括一种有助于测试的方法。这个想法几乎完全消失了。使用某种测试框架分别实施测试是行业惯例。

值得一提的是,“精简编程”(由Knuth构想)从未在软件工程行业中流行。


4
+1可读性问题-测试代码可能比实现代码成比例地更大,尤其是在OO设计中。
Fuhrmanator

2
+1指出使用测试框架。我无法想象在生产代码中同时使用良好的测试框架。
joshin4colours

1
RE:您可能不希望您的客户/客户看到您的测试代码。(我不喜欢这个原因……但是,如果您正在开发一个封闭源代码项目,则无论如何测试代码都不太可能对客户有所帮助。) -可能需要在客户端计算机上运行测试。运行测试可帮助快速确定哪些问题,并在客户ENV ID区别..
sixtyfootersdude

1
@sixtyfootersdude-那是一个非常不寻常的情况。并且假设您正在开发封闭源代码,则您不希望在标准二进制发行版中包含测试,以防万一。(您将创建一个单独的捆绑包,其中包含您要客户运行的测试。)
Stephen C

1
1)您是否错过了我给出三个实际原因的答案的第一部分?.... 2)您是否错过了第二部分,我曾经说过Java程序员曾经这样做过,但是现在不知道了吗?显然,程序员有充分的理由停止这样做了……?
斯蒂芬·C

14

实际上,您可以考虑按合同设计。问题是大多数编程语言都不允许您这样写代码:(手动测试前置条件非常容易,但是后置条件是不改变编写代码方式的真正挑战(一个巨大的负面IMO)。

Michael Feathers对此进行了介绍,这是他提到的可以提高代码质量的多种方式之一。


13

出于许多相同的原因,您试图避免代码中的类之间紧密耦合,所以避免测试与代码之间不必要的耦合也是一个好主意。

创建:测试和代码可由不同的人在不同的时间编写。

控制:如果使用测试来指定需求,那么您肯定希望它们受制于不同的规则,即谁可以更改它们以及何时更改它们,而不是实际的代码。

可重用性:如果将测试内联,则不能将它们与另一段代码一起使用。

想象一下,您有一大堆代码可以正确完成工作,但是无论在性能,可维护性方面,还是有很多不足之处。您决定用新的和改进的代码替换该代码。使用相同的测试集可以帮助您验证新代码是否产生与旧代码相同的结果。

选择性:将测试与代码分开可以更轻松地选择要运行的测试。

例如,您可能有一小套测试,它们仅与您当前正在处理的代码相关,而另一套测试则用于测试整个项目。


我为您的原因感到困惑:TDD已经说过,测试创建是在生产代码之前(或同时)进行的,并且必须由相同的编码器完成!他们还暗示测试非常像需求。当然,如果您不订阅TDD教条(可以接受,但必须明确说明!),这些异议就不适用。另外,“可重用”测试到底是什么?根据定义,不是针对测试的代码进行测试吗?
Andres F.

1
@AndresF。不,测试不是特定于他们要测试的代码。它们特定于他们要测试的行为。因此,假设您已经完成了一个Widget模块,其中包含一组测试,这些测试可以验证Widget的行为是否正确。您的同事想出了BetterWidget,它声称可以完成与Widget相同的功能,但速度要快三倍。如果以与Literate Programming将文档嵌入源代码中的方式相同的方式将Widget的测试嵌入到Widget的源代码中,则无法很好地将这些测试应用于BetterWidget以验证其行为与Widget相同。
加勒布

@AndresF。无需指定您不遵循TDD。这不是宇宙的默认值。至于重用点。测试系统时,您关心的是输入和输出,而不是内部。然后,当您需要创建一个性能与新系统相同但实现方式不同的新系统时,最好具有可以在新旧系统上运行的测试。这不止一次地发生在我身上,有时您需要在旧系统仍处于生产状态时使用新系统,或者甚至并排运行它们。看看Facebook通过反应测试测试“反应纤维”以达到均等的方式。
user1852503

10

这是我想到的一些其他原因:

  • 将测试放在单独的库中可以更轻松地仅将库与您的测试框架链接,而不能与您的生产代码链接(某些预处理程序可以避免这种情况,但是当更简单的解决方案是在其中编写测试时,为什么要构建这样的东西呢?一个单独的地方)

  • 函数,类,库的测试通常是从“用户”的角度(该函数/类/库的用户)编写的。这种“使用代码”通常写在单独的文件或库中,并且如果模拟这种情况,则测试可能会更清晰或更“现实”。


5

如果测试是内联的,则在将产品交付给客户时,有必要删除测试所需的代码。那么,你存储你的测试一个额外的地方简单的代码之间的分离,你需要和你的代码的客户需求。


9
不是不可能。就像LP一样,这将需要一个额外的预处理阶段。例如,可以使用C或JS编译语言轻松完成此操作。
克里斯·德沃罗

+1向我指出。我已经编辑了答案以表示这一点。
mhr 2013年

还假设在每种情况下代码大小都很重要。仅在某些情况下重要并不意味着在所有情况下都重要。在很多环境中,程序员都不会被驱动来优化源代码的大小。如果真是这样,他们将不会创建太多的类。
zumalifeguard

5

在基于对象或面向对象的设计上下文中,这种想法仅相当于“ Self_Test”方法。如果使用像Ada这样的基于对象的已编译语言,编译器将在生产编译期间将所有自检代码标记为未使用(从不调用),因此将对其进行全部优化-它们都不会出现在生成的可执行文件。

使用“ Self_Test”方法是一个非常好的主意,如果程序员真的关心质量,那么他们都会这么做。但是,一个重要的问题是“ Self_Test”方法需要严格的纪律,因为它无法访问任何实现细节,而只能依赖对象规范内的所有其他已发布方法。显然,如果自检失败,则需要更改实现。自检应该严格测试对象方法的所有已发布属性,但决不以任何方式依赖任何特定实现的任何细节。

基于对象的语言和面向对象的语言经常确实针对测试对象外部的方法提供了这种类型的规范(它们强制执行对象的规范,从而阻止了对其实现细节的任何访问,如果检测到任何此类尝试,就会引发编译错误) )。但是,对象自己的内部方法都可以完全访问每个实现细节。因此,自测方法处于一种独特的情况:由于其性质,它必须是一种内部方法(自测显然是被测试对象的一种方法),但是它需要接受外部方法的所有编译器准则(它必须独立于对象的实现细节)。很少有编程语言能够提供训练对象的能力。的内部方法,就好像它是外部方法一样。因此,这是一个重要的编程语言设计问题。

在没有适当的编程语言支持的情况下,最好的方法是创建一个伴随对象。换句话说,对于您编码的每个对象(我们称其为“ Big_Object”),您还将创建第二个伴随对象,其名称由标准后缀和“真实”对象的名称组成(在本例中为“ Big_Object_Self_Test”) ”,其规范包含一个方法(“ Big_Object_Self_Test.Self_Test(This_Big_Object:Big_Object)返回布尔值;”)。然后,伴随对象将取决于主对象的规范,并且编译器将针对伴随对象的实现完全执行该规范的所有规则。


4

这是对大量评论的回应,这些评论表明未进行内联测试,因为很难甚至不可能从发行版中删除测试代码。这是不正确的。几乎所有的编译器和汇编器都已经使用诸如C,C ++,C#之类的编译语言来支持此功能,这是通过所谓的编译器指令完成的。

在c#的情况下(我也相信c ++,语法可能会略有不同,具体取决于您使用的是什么编译器),这就是您可以执行的操作。

#define DEBUG //  = true if c++ code
#define TEST /* can also be defined in the make file for c++ or project file for c# and applies to all associated .cs/.cpp files */

//somewhere in your code
#if DEBUG
// debug only code
#elif TEST
// test only code
#endif

因为这使用了编译器指令,所以如果未设置标志,则代码将不存在于生成的可执行文件中。这也是您为多个平台/硬件制作“编写一次,编译两次”程序的方式。


2

我们在Perl代码中使用内联测试。有一个模块Test :: Inline,可以从内联代码生成测试文件。

我并不是特别擅长组织测试,并且发现它们更容易且更容易在内联时得以维护。

对提出的一些担忧作出回应:

  • 内联测试写在POD部分中,因此它们不是实际代码的一部分。解释器将忽略它们,因此不会出现代码膨胀。
  • 我们使用Vim折叠来隐藏测试部分。您所看到的唯一一件事是,每种测试方法(如)上方只有一行+-- 33 lines: #test----。当您想使用测试时,只需扩展它即可。
  • Test :: Inline模块将测试“编译”为普通的TAP兼容文件,因此它们可以与传统测试共存。

以供参考:


1

Erlang 2实际上支持内联测试。代码中任何未使用的布尔表达式(例如,分配给变量或传递)都将自动视为测试并由编译器评估;如果表达式为假,则代码不会编译。


1

分离测试的另一个原因是,与实际实现相比,您经常使用其他甚至不同的库进行测试。如果混合测试和实现,编译器将无法捕获实现中测试库的意外使用。

此外,测试往往比其测试的实现部分具有更多的代码行,因此您将很难在所有测试之间找到实现。:-)


0

这不是真的 当生产代码时,尤其是在生产例程是纯净的时候,最好将单元测试与生产代码一起放置。

例如,如果要在.NET下进行开发,则可以将测试代码放入生产程序集中,然后在发货之前使用Scalpel删除它们。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.