如何更好地测试自己的代码


45

我是一个相对较新的软件开发人员,我认为我应该改进的一件事是测试自己的代码的能力。每当我开发一项新功能时,我都会发现很难遵循所有可能的途径,以便发现错误。我倾向于遵循一切正常的道路。我知道这是程序员遇到的一个众所周知的问题,但是我们现任雇主没有测试人员,而我的同事们似乎在这方面做得很好。

在我的组织中,我们既不进行测试驱动的开发,也不进行单元测试。这将对我有很大帮助,但这不太可能会改变。

你们认为我可以做些什么来克服这个问题?测试自己的代码时使用什么方法?


28
仅因为您的组织不使用TDD或单元测试,并不表示您不能,只要您继续按时完成并产生高质量的代码即可。
Thomas Owens

1
我猜托马斯击败了我,但是我处于类似的情况。我对代码更改应该做的事情寄予很高的期望,并在可能或何时编写单元测试(即使我们公司未正式进行单元测试)。您不必提交它们,它们是学习功能应如何起作用(或在修复它们后应起作用)的好方法。
布莱恩

6
@Brian,我认为无论其他人当前是否使用它们,都应提交它们。表现出良好的作法也许会让其他人效仿。
CaffGeek

Answers:


20

编码员的工作是建造东西。

测试员的工作是破坏事物。

最困难的是破坏刚构建的东西。只有克服这种心理障碍,您才能成功。


27
-1编码器的工作是建立东西的工作。这总是涉及一些测试。我同意需要单独的测试人员角色,但这不是唯一的防线。
马修·罗达图斯

8
@Matthew Rodatus-编码器方面涉及的测试数量仅用于验证应正常工作的内容。在测试人员方面,目标是发现错误,而不是观察代码是否有效。
mouviciel 2011年

2
那与您的答案不同,我也同意这一点。但是,我仍然不同意。编写质量代码是通过实践来实现的,因为您学会了提前思考-失败的可能性。您不会学会思考失败的可能性,而不会学会创造那些可能性。我认为编码员不是唯一的防线,但他们应该是第一防线。危急关头是工艺的彻底性和精通性之一。
马修·罗达图斯

2
@mouviciel-错误的二分法。编码人员的工作是构建可以正常工作的事物,而他则通过先验思考他的代码应该在什么条件下工作来做到这一点。至少通过创建破坏性测试+进行一些临时边界分析(至少也是)进行了验证。此外,好的编码器还可以针对规范进行工作,并且规范(有效时)始终可以进行测试。因此,优秀的编码人员可以开发测试来验证代码中是否满足这些要求(通常,您可以通过编写最初会失败的要求测试来完成测试,直到您的代码通过测试为止。)
luis.espinal,2011年

2
@戴夫·拉斯利(Dave Lasley)-这恰恰是我的观点:建筑师不是拆毁房屋的最佳人选:他为能够看到其缺陷而强大到极高水平而感到自豪。只有另一位建筑师(而不是街上的那个家伙)才能客观地看待房屋,并发现房屋可能会在某些特定条件下破损,而这是前建筑师实在无法想象的。
mouviciel 2011年

14

弗朗索索,根据您的意见,我将在此处做出一些假设:

“我们既不进行TDD也不进行单元测试。这将对我有很大帮助,但这种情况不太可能改变。”

由此看来,我怀疑您的团队没有在测试上投入太多价值,或者管理层不会为团队花费时间来整理现有代码并使技术负担降至最低。

首先,您需要说服您的团队/管理层进行测试的价值。保持外交。如果管理层使您的团队不断前进,则需要向他们展示一些事实,例如每个版本的缺陷率。花费在修复缺陷上的时间可以更好地花在其他方面,例如改进应用程序并使它更适应未来的需求。

如果团队和管理层总体上对修改代码无动于衷,并且对此感到不满意,则可能需要寻找另一个工作地点,除非您可以像我所说的那样说服他们。在我工作过的所有地方,我都不同程度地遇到了这个问题。从缺少适当的域模型到团队沟通不畅,都是如此。

关心代码和所开发产品的质量是一个很好的属性,并且您一直希望在其他人中得到鼓励。


11

如果您使用C,Objective-C或C ++进行编码,则可以使用CLang Static Analyzer批判您的源代码,而无需实际运行它。

有一些可用的内存调试工具:ValGrind,Mac OS X上的Guard Malloc,* NIX上的Electric Fence。

一些开发环境提供了使用调试内存分配器的选项,该分配器执行诸如用垃圾填充新分配的页面和新释放的页面,检测未分配的指针的释放以及在每个堆块之前和之后写入一些数据的事情,其中​​调试器是如果该数据的已知模式发生变化,则调用。

Slashdot上的一些人说,他从调试器中单步执行的全新源代码行中获得了很多价值。“就是这样。”他说。我并不总是听从他的建议,但是当我得到他的建议时,对我很有帮助。即使您没有激发异常代码路径的测试用例,也可以在调试器中旋转一个变量以采用此类路径,例如通过分配一些内存,然后使用调试器将新指针设置为NULL而不是内存地址,然后逐步完成分配失败处理程序。

使用断言-C,C ++和Objective-C中的assert()宏。如果您的语言不提供断言功能,请自己编写。

自由使用断言,然后将其保留在代码中。我将assert()称为“不断测试的测试”。我最常使用它们在大多数函数的入口处检查前提条件。这是“按合同编程”的一部分,它是Eiffel编程语言中内置的。另一部分是后置条件,即在函数返回点使用assert(),但是我发现我没有得到比前提条件更多的里程。

您还可以使用assert来检查类不变式。虽然没有严格要求所有类具有任何不变式,但是最明智地设计的类确实具有它们。类不变式是某些条件,该条件总是正确的,而不是在成员函数内部,这可能会使您的对象暂时处于不一致状态。此类函数始终必须在返回之前恢复一致性。

因此,每个成员函数都可以在进入和退出时检查不变量,并且该类可以定义一个称为CheckInvariant的函数,任何其他代码都可以在任何时间调用。

使用代码覆盖率工具检查源中的哪些行实际受到测试,然后设计能够激发未经测试的行的测试。例如,您可以通过在配置了很少物理内存,没有交换文件或很小的虚拟机的VM中运行应用程序来检查低内存处理程序。

(出于某种原因,我从来都不愿意使用,尽管BeOS可以在没有交换文件的情况下运行,但那样的话非常不稳定。编写BFS文件系统的Dominic Giampaolo敦促我不要在没有交换的情况下运行BeOS。我没有看看为什么这很重要,但这一定是某种实现工件。)

您还应该测试代码对I / O错误的响应。尝试将所有文​​件存储在网络共享上,然后在应用程序工作量很大时断开网络电缆的连接。同样,如果通过网络进行通信,则断开电缆连接-或关闭无线设备。

我发现特别令人生气的一件事是没有健壮的Javascript代码的网站。Facebook的页面加载了数十个小的Javascript文件,但是如果其中任何一个未能下载,整个页面就会中断。只能通过某种方式提供某种容错功能(例如通过重试下载),或者在某些脚本没有下载时提供某种合理的备用。

在编写大型重要文件的过程中,尝试使用调试器或* NIX上的“ kill -9”杀死您的应用程序。如果您的应用程序具有良好的结构,则整个文件将被写入或根本不会被写入,或者如果仅部分写入,则写入的内容将不会被破坏,保存的数据将完全可用重新读取文件后的应用程序。

数据库始终具有容错磁盘I / O,但几乎没有其他任何类型的应用程序具有。日记文件系统可以防止在电源故障或崩溃时文件系统损坏,但它们根本不会采取任何措施来防止最终用户数据损坏或丢失。这是用户应用程序的责任,但除数据库之外几乎没有其他任何方式可以实现容错。


1
+1不需要其他人支持的实用建议。我唯一要补充的是,assert是用于记录和检查条件的,除非代码中存在错误,否则条件不会失败。永远不要断言事情会如必要的文件会由于“运气不好”,没有被发现,或者无效的输入,等等
伊恩Goldby

10

当我考虑测试代码时,通常会经历一系列思考过程:

  1. 如何将这种“东西”分解成可测试的大小块?如何隔离我要测试的内容?我应该创建哪些存根/模拟?
  2. 对于每个块:如何测试该块以确保其对合理的一组正确输入做出正确响应?
  3. 对于每个块:如何测试块对错误输入(NULL指针,无效值)的正确响应?
  4. 如何测试边界(例如,值从有符号变为无符号,从8位到16位等)?
  5. 我的测试覆盖代码的程度如何?我错过了什么条件吗?[这是代码覆盖工具的好地方。]如果遗漏了某些代码,并且永远无法执行,那么确实需要在那里吗?[还有另一个问题!]

我发现最简单的方法是开发测试以及代码。一旦编写了代码片段,我就喜欢为其编写测试。在编写了几千行具有非平凡的循环代码复杂性的代码之后,尝试进行所有测试是一场噩梦。在添加几行代码之后再添加一两个测试确实很容易。

顺便说一句,除非您所在的公司和/或您的同事没有进行单元测试或TDD,但这并不意味着您不能尝试它们,除非特别禁止。也许使用它们来创建健壮的代码将是其他人的一个很好的例子。


5

除了其他答案中给出的建议外,我建议使用静态分析工具(维基百科提供了多种针对各种语言静态分析工具的列表),以便在测试开始之前发现潜在的缺陷并监视与之相关的某些指标代码的可测试性,例如圈复杂度Halstead复杂度度量以及内聚和耦合(您可以使用扇入和扇出测量它们)。

找到难以测试的代码并使其更易于测试将使您更容易编写测试用例。另外,及早发现缺陷将为整个质量保证实践(包括测试)增加价值。从这里开始,熟悉单元测试工具和模拟工具将使您更轻松地实施测试。


1
+1为圈复杂度。“我发现遵循所有可能的路径确实很困难,因此我可以找到错误”对我意味着,OP的代码可能需要分解成较小,较不复杂的块。
托比,

@Toby是的,这就是为什么我决定进行静态分析的原因。如果您无法测试代码,则可能会遇到问题。而且,如果您的代码有一个问题,那么可能还会有其他问题。使用工具查找潜在的警告标记,对其进行评估,然后根据需要进行更正。您不仅将拥有更多可测试的代码,而且还将拥有更多可读的代码。
Thomas Owens

3

您可以研究真值表的可能用法,以帮助您定义代码中所有可能的路径。不可能考虑复杂功能中的所有可能性,但是一旦为所有已知路径建立了处理,就可以为else情况建立处理。

但是,这种特殊能力中的大多数都是通过经验学习的。在使用了一定时间的框架之后,您将开始看到行为的模式和专用标记,这些行为和标记使您可以查看一段代码,并查看微小的更改可能在哪里导致重大错误。我能想到的在这种情况下提高自己才能的唯一方法是练习。


3

如果您像您所说的那样不需要单元测试,那么,没有比尝试手动破坏自己的代码更好的方法了。

尝试将您的代码推到极限。例如,尝试将变量传递给超出边界限制的函数。您是否具有应该过滤用户输入的功能?尝试输入不同的字符组合。

考虑用户的观点。尝试成为将使用您的应用程序或函数库的用户之一。


1
+1表示从用户的角度看事物。
马修·罗达图斯

3

但我们目前的雇主没有测试员,而我的同事似乎在这方面做得很好

您的同事必须真正出色,不遵循TDD或单元测试,并且永远不会产生错误,因此在某种程度上,我怀疑他们自己不会执行任何单元测试。

我猜想您的同事正在进行的测试多于允许进行的测试,但是由于管理层不了解这一事实,因此组织遭受了损失,因为管理层感觉到没有在进行真正的测试并且错误数量很少,因此测试并不重要,因此不会安排时间。

与您的同事交谈,并尝试弄清楚他们正在执行哪种单元测试并进行模拟。以后,您可以为单元测试和TDD属性提供更好的原型方法,然后将这些概念缓慢地引入团队,以便于采用。


2
  • 在编写代码之前,先编写测试。
  • 每当您修复测试未捕获的错误时,请编写测试以捕获该错误。

即使您的组织没有完整的报道,您也应该能够覆盖所写内容。就像编程中的许多事情一样,一次又一次地执行代码的经验是提高其效率的最佳方法之一。


2

除了所有其他注释之外,既然您说您的同事擅长编写非快乐路径测试,那么为什么不请他们与您配对以编写一些测试。

最好的学习方法是看它是如何完成的,并从中学到东西。


2

黑匣子测试!您应该在考虑测试的情况下创建类/方法。您的测试应基于软件的规范,并应在序列图上明确定义(通过用例)。

现在,由于您可能不想进行测试驱动的开发...

对所有传入数据进行输入验证;不要相信任何人。.net框架基于无效参数,空引用和无效状态引发了许多异常。您应该已经在考虑在UI层上使用输入验证,因此这与中间件中的技巧相同。

但是您确实应该进行某种自动化测试。这些东西可以挽救生命。


2

在我的经验中

测试单元,如果不是全自动的,那就没用了。这更像是一个尖头毛老板可以买的东西。为什么?,因为Test Unit承诺您可以节省一些自动化测试过程的时间和金钱。但是,某些测试单元工具却相反,它迫使程序员以某种怪异的方式工作,并迫使其他人创建过度扩展的测试。在大多数情况下,这不会节省工作时间,但会增加从质量检查到开发人员的时间。

UML浪费时间。一支白板+笔可以完成相同,更便宜,更快速的操作。

顺便说一句,如何擅长编码(并避免错误)?

  • a)原子性。一个函数可以完成一个简单的任务(或几个单独的任务)。因为它易于理解,所以易于跟踪和解决。

  • b)同源性。例如,如果您使用存储过程调用数据库,则对其余代码进行处理。

  • c)识别,减少和隔离“创意代码”。大多数代码几乎都是复制和粘贴。相反,创意代码是新代码,它充当原型,可能会失败。该代码容易产生逻辑错误,因此减少,隔离和识别它很重要。

  • d)“薄冰”代码是您知道它“不正确”(或潜在危险)但仍需要的代码,例如用于多任务处理的不安全代码。尽量避免。

  • e)避免使用黑匣子代码,这包括您未完成的代码(例如框架)和正则表达式。用这种代码很容易错过一个错误。例如,我在一个使用Jboss的项目中工作,发现Jboss中没有一个错误,而是10个错误(使用最新的稳定版本),找到这些错误是PITA。避免特别使用Hibernate,因为它会隐藏实现,因此会导致bug。

  • f)在代码中添加注释。

  • g)用户输入作为错误的来源。识别它。例如,SQL注入是由用户输入引起的。

  • h)识别团队的不良要素,并分离分配的任务。一些程序员倾向于拧紧代码。

  • i)避免不必要的代码。例如,如果该类需要Interface,则使用它,否则避免添加不相关的代码。

a)和b)是关键。例如,我的系统出现问题,当我单击按钮(保存)时,它没有保存表单。然后我做了一个清单:

  • 该按钮有效吗?
  • 数据库存储什么?不,因此错误处于中间步骤。
  • 那么,存储在数据库中的类是否起作用?否<-好的,我发现了错误。(这与数据库权限有关)。然后,我不仅检查了此过程,还检查了每个执行相同操作的过程(因为代码的同源性)。我花了5分钟来跟踪该错误,花了1分钟来解决它(以及许多其他错误)。

和一个旁注

大多数时候,QA很糟糕(作为Dev的单独部分),如果他们不在项目中工作,那将毫无用处。他们进行一些通用测试,仅此而已。他们无法识别大多数逻辑错误。以我为例,我在一家著名的银行工作,一名程序员完成了代码,然后将其发送给质量检查人员。质量检查人员批准了该代码并将其投入生产...然后,该代码失败了(史诗般的失败),您知道是谁的责任吗?是的,程序员。


2

测试人员和程序员从不同的角度面对问题,但是两个角色都应充分测试功能并查找错误。角色不同之处是重点。传统的测试人员只能从外部(即黑匣子)看到应用程序。他们是应用程序功能要求方面的专家。程序员应该既是功能要求又是代码方面的专家(但往往会更专注于代码)。

(这取决于组织是否明确要求程序员是需求专家。无论如何,隐含的期望在那里-如果您设计错了,您(而不是需求人员)会受到指责。)

这种双重专家的角色使程序员感到费解,除了最有经验的人之外,这可能会降低要求的熟练程度。我发现我必须在思维上换档,以考虑该应用程序的用户。这对我有帮助:

  1. 调试 ; 在代码中设置断点并运行应用程序。遇到断点后,在与应用程序交互时逐步进行操作。
  2. 自动化测试 ; 编写测试您的代码的代码。这仅对UI下方的层有用。
  3. 了解你的测试员 ; 他们可能比您更了解该应用程序,因此请向他们学习。询问他们应用程序的弱点是什么,以及他们使用什么策略来发现错误。
  4. 了解您的用户 ; 学习如何穿上用户的鞋子。功能要求是用户的指纹。您的用户通常对应用程序有很多了解,而功能需求可能并不清楚。随着您更好地了解用户-他们在现实世界中的工作性质以及您的应用程序应该如何帮助他们-您会更好地了解该应用程序应该是什么。

2

我认为您想在两个方面开展工作。一种是政治的,使您的组织在某种程度上采用测试(希望随着时间的推移,他们将采用更多的测试)。与您工作场所外的质量检查工程师联系。查找质量检查书籍清单。在相关的维基百科文章中查找。熟悉质量检查原则和惯例。学习这些内容将使您准备好在组织中提出最有说服力的案例。好的质量检查部门确实存在,并且确实为组织增加了可观的价值。

作为个人开发人员,请采用可用于您自己工作中的策略。通过共同开发代码和测试自己使用TDD。保持测试清晰并保持良好状态。如果被问到为什么要这样做,可以说您正在防止回归,这可以使您的思维过程更好地组织起来(两者都是正确的)。编写可测试的代码是一门艺术,要学习它。对您的开发人员来说是一个很好的例子。

在某种程度上,我在这里向自己宣讲,因为我做这些东西的方式比我应该知道的要少。

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.