测试与调试

458 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情

执行测试

测试通常被认为是分两个阶段进行的。人们应该总是从单元测试开始。在此阶段,测试人员构建并运行旨在确定各个代码单元(例如函数)是否正常工作的测试。接下来是集成测试,旨在确定单元组在组合时是否正常工作。最后,功能测试用于检查程序作为一个整体是否按预期运行。在实践中,测试人员会循环执行这些阶段,因为集成或功能测试期间的故障会导致对单个单元进行更改。

功能测试几乎总是最具挑战性的阶段。整个程序的预期行为要困难得多表征其每个部分的预期行为。

例如,表征字处理器的预期行为比表征计算文档中字符数的子系统的行为更具挑战性。规模问题也可能使功能测试变得困难。功能测试需要数小时甚至数天才能运行,这并不罕见。

许多工业软件开发组织都有一个软件质量保证(SQA)小组,该小组独立于负责实施软件的小组。SQA组的任务是确保在软件发布之前,它适合其预期目的。在某些组织中,开发组负责单元测试,QA 组负责集成和功能测试。

在工业中,测试过程通常是高度自动化的。测试人员51不会坐在终端上输入输入和检查输出。相反,他们使用自主测试驱动程序

  • 设置调用程序(或单元)进行测试所需的环境。

  • 调用程序(或单元)以使用预定义或自动生成的输入序列进行测试。

  • 保存这些调用的结果。检查测试结果的可接受性。.

准备适当的报告。

在单元测试期间,我们经常需要构建存根和驱动程序。驱动程序模拟使用被测单元的程序部分,而存根模拟被测单元使用的程序部分。存根很有用,因为它们允许人们测试依赖于软件的单元,有时甚至是还不存在的硬件。这允许程序员团队同时开发和测试系统的多个部分。

理想情况下,存根应该

  • 检查调用方提供的环境和参数的合理性(调用具有不适当参数的函数是一个常见错误)。

  • 以与规范一致的方式修改参数和全局变量。

  • 返回与规范一致的值。

构建足够的存根通常是一个挑战。如果存根替换的单元旨在执行某些复杂的任务,则构建执行与规范一致的操作的存根可能等同于编写存根旨在替换的程序。克服此问题的一种方法是限制存根接受的参数集,并创建一个表,其中包含要为要在测试套件中使用的每个参数组合返回的值。

自动化测试过程的一个吸引力是它有助于回归测试。当程序员尝试调试程序时,安装一个“修复程序”来破坏过去工作的东西,或者许多东西,是很常见的。每当进行任何更改时,无论多么小,您都应该检查程序是否仍然通过了它曾经通过的所有测试。

调试

有一个迷人的城市传说,关于修复软件缺陷的过程如何被称为调试。图 8-2 中的照片是 1947 年 9 月 9 日哈佛大学 Mark II Aiken 继电器计算器研究小组的实验室书籍中的一页。请注意贴在页面上的飞蛾及其下方的短语“发现第一个实际错误案例”。

image.png

有些人声称,发现被困在Mark II中的不幸飞蛾导致了使用短语调试。然而,“发现错误的第一个实际案例”的措辞表明,对该短语的字面解释已经很常见。52岁的马克二号项目负责人格蕾丝·默里·霍珀(Grace Murray Hopper)明确表示,“bug”一词已经广泛用于描述第二次世界大战期间电子系统的问题。在此之前,霍金斯的《电学新教理问答》是一本1896年的电气手册,其中包括这样一个条目:“'bug'一词在有限的程度上用于表示电气设备连接或工作中的任何故障或故障。在英语用法中,“bugbear”这个词的意思是“任何引起看似不必要的或过度的恐惧或焦虑的东西”。53 莎士比亚似乎把这句话缩短为“虫子”,当时哈姆雷特对“我生命中的虫子和妖精”赞不绝口。

使用“bug”这个词有时会导致人们忽略一个基本事实,即如果你写了一个程序,它有一个“bug”,你就搞砸了。Bug不会毫无争议地爬进完美的程序中。如果你的程序有一个错误,那是因为你把它放在那里。错误不会在程序中繁殖。如果您的程序有多个错误,那是因为您犯了多个错误。

运行时错误可以按两个维度进行分类:

  • 显性→隐蔽的:明显的错误有明显的表现形式,例如,程序崩溃或运行时间(也许是永远)比它应该的要长得多。隐蔽的虫子没有明显的表现。程序可能会运行到结论,除了提供不正确的答案之外,没有任何问题。许多错误介于两个极端之间,错误是否明显取决于您对程序行为的仔细检查程度。

  • 持久性→间歇性:每次使用相同的输入运行程序时,都会发生持久性错误。间歇性错误仅在某些时候发生,即使程序在相同的输入上运行并且似乎在相同的条件下运行。当我们进入第16章时,我们将研究一些程序,这些程序模拟了随机性发挥作用的情况。在此类程序中,间歇性错误很常见。

最好的错误是公开和持久的。开发人员对部署程序的可取性不抱任何幻想。如果其他人愚蠢到试图使用它,他们很快就会发现自己的愚蠢。也许程序会在崩溃之前做一些可怕的事情,例如,删除文件,但至少用户有理由担心(如果不是恐慌的话)。优秀的程序员试图以这样的方式编写他们的程序,即编程错误会导致既明显又持久的错误。这通常称为防御性编程。

进入不可取性坑的下一步是明显但间歇性的错误。一个几乎一直计算飞机正确位置的空中交通管制系统将比一个一直犯明显错误的系统危险得多。一个人可以生活在一个傻瓜的天堂里一段时间,也许可以部署一个包含有缺陷的程序的系统,但这个错误迟早会变得明显。如果促使 Bug 变得明显的条件很容易重现,则跟踪和修复问题通常相对容易。如果引起虫子的条件不明确,生活就会更加艰难。

以隐蔽方式失败的程序通常非常危险。由于它们没有明显的问题,人们使用它们并相信它们会做正确的事情。社会越来越依赖软件来执行关键计算,这些计算超出了人类执行甚至检查正确性的能力。因此,程序可以在很长一段时间内提供未检测到的谬误答案。这样的程序可以,而且已经造成了很大的损害。一个评估抵押债券投资组合风险并自信地吐出错误答案的计划可能会让银行(也许还有整个社会)陷入很多麻烦。飞行管理计算机中的软件可以决定飞机是否停留在空中.55放射治疗机提供的辐射比预期的多一点或少一点,可能是癌症患者生死的区别。一个只是偶尔犯隐蔽错误的程序可能会或可能不会比总是犯这种错误的程序造成更少的破坏。隐蔽和间歇性的错误几乎总是最难发现和修复的。

学习调试

调试是一项学习技能。没有人本能地把它做好。好消息是,学习起来并不难,而且是一种可转移的技能。用于调试软件的相同技能可用于找出其他复杂系统的问题,例如实验室实验或病人。

至少四十年来,人们一直在构建称为调试器的工具,并且调试工具内置于所有流行的Python IDE中。(如果还没有,请尝试使用 Spyder 中的调试工具。这些工具可以提供帮助。但更重要的是你如何处理这个问题。许多有经验的程序员甚至不打扰调试工具,而是依靠 print 语句。

当测试表明程序以不良方式运行时,将开始调试。调试是搜索该行为的解释的过程。始终如一地擅长调试的关键是系统地进行搜索。

首先研究可用数据。这包括测试结果和程序文本。研究所有测试结果。不仅要检查揭示问题存在的测试,还要检查那些似乎完美工作的测试。试图理解为什么一个测试有效而另一个测试不起作用通常很有启发性。查看程序文本时,请记住,您并不完全理解它。如果你这样做了,可能不会有错误。

接下来,形成一个您认为与所有数据一致的假设。假设可以像“如果我将第403行从x< y更改为x < = y,问题就会消失”,也可以像“我的程序不起作用,因为我忘记了在多个地方别名的可能性”一样宽泛。

接下来,设计并运行一个可重复的实验,有可能反驳假设。例如,您可以在每个循环之前和之后放置一个 print 语句。如果它们总是成对的,那么循环导致非终止的假设已被驳斥。在运行实验之前,请确定如何解释各种可能的结果。所有人都受到心理学家所谓的确认偏见的影响 - 我们以一种强化我们想要相信的方式解释信息。如果你等到运行实验后再考虑结果应该是什么,你更有可能成为一厢情愿的牺牲品。

最后,记录你尝试过的实验。当你花了很多时间更改代码试图追踪一个难以捉摸的错误时,很容易忘记你已经尝试过的东西。如果你不小心,你可能会浪费太多时间一遍又一遍地尝试相同的实验(或者更有可能是一个看起来不同但会给你相同信息的实验)。请记住,正如许多人所说,“精神错乱就是一遍又一遍地做同样的事情,但期待不同的结果。56