重新设计时,如何有效地保持测试工作?


14

一个经过良好测试的代码库有很多好处,但是测试系统的某些方面会导致代码库可以抵抗某些类型的更改。

一个示例是测试特定的输出-例如文本或HTML。通常(天真吗?)编写测试以期望将特定文本块作为某些输入参数的输出,或者在块中搜索特定部分。

更改代码的行为以满足新的要求,或者由于可用性测试导致了界面的更改,因此也需要更改测试,甚至可能不是针对更改代码的特定单元测试的测试。

  • 您如何管理查找和重写这些测试的工作?如果您不能只是“全部运行并让框架将它们整理出来”怎么办?

  • 还有哪些其他类型的被测代码会导致习惯性的易碎测试?



4
这个问题被错误地问到有关重构的问题-单元测试在重构下应该是不变的。
Alex Feinman 2010年

Answers:


9

我知道TDD的人们会讨厌这个答案,但是对我来说,很大一部分是要仔细选择在哪里测试某些东西。

如果我对较低层的单元测试太过疯狂,那么不更改单元测试就无法做出有意义的改变。如果界面从不公开,并且不打算在应用程序外部重用,那么对于其他情况下的快速更改而言,这只是不必要的开销。

相反,如果您尝试更改的内容暴露或重复使用,那么您必须更改的每个测试都表明您可能在其他地方破坏了某些东西。

在某些项目中,这可能等于从验收层向下设计测试,而不是从单元测试向上进行设计。并减少了单元测试和更多的集成样式测试。

这并不意味着您仍无法识别单个功能和代码,直到该功能满足其接受标准。这只是意味着在某些情况下,您最终不会使用单元测试来衡量验收标准。


我认为您的意思是写“模块外部”,而不是“应用程序外部”。
SamB

SamB,这取决于。如果该接口是一个应用程序在少数地方的内部接口,但不是公共接口,那么我认为如果该接口可能易失,我会考虑进行更高级别的测试。
比尔

我发现这种方法与TDD非常兼容。我喜欢从更接近最终用户的应用程序的上层开始,因此我可以知道下层如何使用下层来设计下层。从本质上讲,自上而下的构建使您可以更准确地设计一层与另一层之间的接口。
格雷格·伯格哈特

4

我刚刚完成了SIP堆栈的大修,重写了整个TCP传输。(相对于大多数重构,这是一个相当大规模的近重构。)

简而言之,有一个TIdSipTcpTransport,它是TIdSipTransport的子类。所有TIdSipTransports共享一个通用的测试套件。TIdSipTcpTransport的内部有许多类-包含连接/启动消息对,线程化TCP客户端,线程化TCP服务器等的映射。

这是我所做的:

  • 删除了我要替换的类。
  • 删除了这些类的测试套件。
  • 测试套件特有的TIdSipTcpTransport(并仍有测试套件共同所有TIdSipTransports)。
  • 运行TIdSipTransport / TIdSipTcpTransport测试,以确保它们均失败。
  • 注释掉除了一个TIdSipTransport / TIdSipTcpTransport测试之外的所有测试。
  • 如果需要添加一个类,则可以添加它编写测试,以构建足以使唯一未注释的测试通过的功能。
  • 泡沫,冲洗,重复。

因此,我以注释掉的测试(*)的形式知道了我仍然需要做的事情,并且由于我编写了新的测试,我知道新代码可以按预期工作。

(*)确实,您不需要将它们注释掉。只是不要运行它们。100个失败的测试不是很令人鼓舞。另外,在我的特定设置中,编译更少的测试意味着更快的测试-写入-重构循环。


我几个月前已经做过,对我来说效果很好。但是,当与同事配对进行领域模型模块的彻底重新设计时,我不能绝对地应用此方法(这反过来又触发了项目中所有其他模块的重新设计)。
Marco Ciambrone 2011年

3

当测试脆弱时,我通常会发现它是因为我正在测试错误的东西。以HTML输出为例。如果检查实际的HTML输出,则测试将很脆弱。但是您对实际输出不感兴趣,对它是否传达应有的信息感兴趣。不幸的是,这样做需要对用户的大脑内容进行断言,因此不能自动完成。

您可以:

  • 生成HTML作为冒烟测试以确保其实际运行
  • 使用模板系统,因此您可以测试模板处理器和发送到模板的数据,而无需实际测试确切的模板本身。

SQL也发生同样的事情。如果您断言实际的SQL,则您的类将使您陷入麻烦。您真的想断言结果。因此,在单元测试期间,我使用SQLITE内存数据库来确保我的SQL确实完成了它应该做的事情。


使用结构HTML也可能有所帮助。
SamB

@SamB当然会有所帮助,但我认为这不会完全解决问题
Winston Ewert 2010年

当然不是,什么也不能:-)
SamB

-1

首先创建一个新的API,它可以实现您希望的新API行为。如果碰巧这个新的API与OLDER API具有相同的名称,那么我将名称_NEW附加到新的API名称中。

int DoSomethingInterestingAPI();

变成:

int DoSomethingInterestingAPI_NEW(int take_more_arguments); int DoSomethingInterestingAPI_OLD(); int DoSomethingInterestingAPI(){DoSomethingInterestingAPI_NEW(whatever_default_mimics_the_old_API); 好的-在这个阶段-您所有的回归测试都通过-使用名称DoSomethingInterestingAPI()。

下一步,遍历您的代码,并将对DoSomethingInterestingAPI()的所有调用更改为DoSomethingInterestingAPI_NEW()的相应变体。这包括更新/重写需要更改回归测试的任何部分才能使用新的API。

接下来,将DoSomethingInterestingAPI_OLD()标记为[[deprecated()]]。只要您愿意,就保留不推荐使用的API(直到您安全地更新了可能依赖该API的所有代码)。

使用这种方法,您的回归测试中的任何失败都只是该回归测试中的错误,或者恰恰是您想要的,可以识别代码中的错误。通过显式创建API的_NEW和_OLD版本修改API的分阶段过程,使您可以将新代码和旧代码的一部分同时存在。

这是实践中这种方法的一个好(硬)示例。我有功能BitSubstring()-在这里我使用了让第三个参数是子字符串中的COUNT位的方法。为了与C ++中的其他API和模式保持一致,我想切换为以该函数的参数开头/结尾。

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

我使用新的API创建了一个函数BitSubstring_NEW,并更新了所有代码以使用该函数(不再给BitSubString打电话)。但是我在实现中留下了数个版本(数月)-并将其标记为已弃用-这样每个人都可以切换到BitSubString_NEW(并且那时将参数从count更改为begin / end样式)。

然后-当该转换完成时,我做了另一个提交,删除了BitSubString()并重命名了BitSubString_NEW-> BitSubString()(并弃用了名称BitSubString_NEW)。


切勿在后缀中添加不带任何含义或对名称不屑一顾的后缀。始终努力给出有意义的名字。
Basilevs

您完全错了。首先-这些不是“不带任何意义”的后缀。它们的含义是API正在从较旧的API过渡到较新的API。实际上,这就是我要回答的问题的全部内容,也是答案的全部内容。一旦转换完成,名称CLEARLY就可以传达OLD API,NEW API以及API的最终目标名称。AND-_OLD / _NEW后缀是临时的-仅在API更改过渡期间。
刘易斯·普林格

三年后,祝您使用API​​的NEW_NEW_3版本好。
Basilevs
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.