作者:陈默涵,邮箱:[email protected]
最后更新时间:2023/12/07
在 ABACUS 软件的发展过程中,测试极其重要。
以下情景纯属虚构,但至少在 ABACUS 软件开发过程中时有发生:
- 故事一:小明是程序 ABC 的积极开发者,有一天他发现已经写好的新功能在之前版本可用,但在最新版本不能用了!这件事情影响了小明目前的进度,只好停下来维修。小明找上级说明事情缘由之后,开始找程序错误的原因,经过一段时间查找(可能是几个小时,可能是几天),发现是另一位开发者小强同学在几日前提交的代码中破坏了这个功能。小明比较郁闷,因为代码发生错误责任不在自己,是别人把它改错的。小强也是有责任心的开发者,收到反馈后尽快修改了错误。但从此小明总是担心自己的代码被破坏,而小强写代码时也更加谨小慎微。
- 故事二:有热心用户小风在用户群里提问,为什么照着文档跑程序,程序就崩了。有责任心的开发者小云看到后回答:“我自己的电脑上能跑,但为什么你的电脑上跑不了?”经过小云地毯式搜索验证(可能是几个小时,可能是几天)找到原因:“因为编译环境不一样导致的,某段语法在另外一个编译器上会得到错误的结果”。小云提出的解决方案是建议小风换个环境,或者等小云忙完手头的事情有空之后,再帮忙解决一下这个问题。小风有点不满这个程序带来的体验,遂决定弃坑。小云也被这额外多出来的工作量搞得身心俱疲。
以上第一个故事来自开发者和开发者,第二个故事来自开发者和用户,小明、小强、小风、小云的故事,就是 ABACUS 软件过去几年不断会上演的故事。当以上事情反复出现时,我们知道不管是开发者还是用户,这些老师同学从自身的角度出发都是积极参与的,那我们不禁要问:
-
到底是哪里出了问题?
-
需要采取什么样的方法?
-
是否可以防止这样的事情再发生?
如果我们软件最终的目标还是希望这个功能能够被更多人更好的使用,而不是仅仅用来申请软件专利,发文章之类的,那么出现功能 bug 后,会产生两个结果。
第一:这些 bug 的发现和修复本质上是要消耗开发者大量时间的!例如小明和小云都花了很久时间去找问题,这个过程他(她)几乎得不到任何其他人的帮助!能否快速找到取决于开发者自身对算法和 bug 的认识程度。有人把复杂程序的排错认为更多是一门艺术而不是技术,“程序高手”可能会更快的定位到问题,但我们认为真正的程序高手可以避免出现这类问题。
第二:如果不采取更为有效的方式,那么修完一次还有可能要修第二次,例如小明的代码又被另一位开发者大强破坏了,或者小云修完之后发现另一位用户大风在别的环境又出错了。这件事情会反复出现,而开发者的热情会在这个过程被一点点磨光。
没有意识到以上两点的开发者(包括所谓的“程序高手”)建议阅读到这里自己静下来想一想。
究竟什么方法才是避免以上问题的最根本方法?答案是两个字,测试!只是如果直接说出来可能大家并不会意识到这个方法本身的重要性。
以上发生的结果其实是软件开发的流程出了问题,当一个软件庞大到一定程度时,许多代码互相影响之后,出 bug 是难免的,发生上面故事里的情节也是时有发生的。
所以,如果时间反正是要消耗的,我们是否可以劝说开发者意识到:在开发到“某个阶段”(什么阶段?)就让新功能的测试尽可能充分和完整,就可以减少将来“维修代码“带来的时间消耗?实际上简单的修复并不能做到一劳永逸。更糟糕的是,经过一段时间,开发者可能对当初开发算法的记忆也变淡了,因此整体修复的效果不见得会比当初做完整来得更好。经年累月的结果是,持续修复这些问题所花的精力和资源实际上是累加到原来功能开发的时间和资源成本上,这样不知不觉整个软件团队实际上需要为整个项目付出更高的时间甚至经济成本买单,而对任何软件资助方来讲,要解释清楚这个成本,解释成本是极高的(就是解释不清楚,理由也不充分)。所以 ABACUS 团队对所有开发者的建议是,尽可能的把写测试的工作乃至为了让代码减少错误所需的重构工作放入功能开发周期内执行,让一个软件的 feature 满足:功能正确、代码规范、框架合理,注释清楚、测试完整。
最后回答的一点是:什么时候添加测试合适?一般来讲,开发者能把一个新功能实现正确是可以带来巨大成就感的一件事情,是能类比打仗的时候把己方旗帜插上对方阵地山头的高光时刻。所以这个时候让开发者加测试一般是不愿意的,或者觉得没必要。在学术界,往往结果对了之后可以进入写文章投稿的阶段,至于程序的维护在发文章前面往往是排后面的。等到文章发完之后,马上又会进入一个新的课题。以上原因也导致一般在高校课题组很难维持发展一个大型软件。
所以添加测试合适的时间,我们对 ABACUS 开发者的建议是:开发完新功能之后,如果确认这个功能会有用,请添加测试,并且在这个整理代码的过程中把上一段话的最后四点补充做完:
- 代码规范,程序的各种命名、注释、代码行的格式等应符合程序开发的命名标准和编码规范。
- 框架合理,如果不合理就请重构代码,趁你的记忆还鲜活。
- 注释清楚,如果有些代码可能别人看不懂,不要吝惜你的语言,用英文注释!
- 测试完整,最后把相应主要的功能函数加上测试,具体怎么加,请看这个系列的文档。
程序测试是软件开发过程中至关重要的一个环节,是一种实际输出和预期结果之间的客观对比,它保证软件能以预期的形式运行,得到预期的结果。软件测试试图最大程度上防范潜在的缺陷和错误。通过对软件进行有效测试,可以降低项目风险,确保软件功能在各种条件下的正确性,提高软件功能的稳定性,甚至提前发现和修复问题,节省软件的维护成本。
ABACUS 作为开源的密度泛函理论项目,涉及到的不同背景开发人员较多,每位开发人员负责开发代码的一部分功能。为了确保软件功能在持续更新的代码中保持正确,我们建议开发人员掌握测试的方法,对每个功能函数编写相应的测试函数。新添加的测试函数会被 ABACUS 加入整个软件的测试流程,一旦有新提交的代码对该功能产生破坏,代码审核人员和代码开发者可以快速得到反馈,从而避免错误或者漏洞的产生而留下隐患。
我们从 https://google.github.io/googletest/primer.html 网页上把关于一个好的测试的标准进行了中文翻译,列举如下:
- 测试需要独立,以及可以被重复。调试一个因其他测试的结果而成功或失败的测试是很麻烦的。GoogleTest 通过在不同对象上运行每一个测试来隔离测试。当一个测试失败时,GoogleTest 允许你单独运行这个测试,以便快速调试。
- 测试需要被好好的组织起来,并且能够反映出被测试代码的结构。GoogleTest 将相关测试归组到测试套件中,这些测试套件可以共享数据和子程序。这种常见模式很容易识别,并且使得测试易于维护。当人们切换项目并开始工作在一个新的代码库上时,这种一致性尤其有帮助。
- 测试应该具有可移植性和可复用性。谷歌拥有大量与平台无关的代码;它的测试也应当是平台中立的。GoogleTest 能够在不同的操作系统上工作,配合不同的编译器,无论是否支持异常处理,因此 GoogleTest 的测试可以适用于多种配置。
- 当测试失败了,该测试应该尽可能多的提供错误信息。 GoogleTest 在遇到第一个测试失败时不会停止。相反,它只会停止当前的测试并继续执行下一个测试。你还可以设置测试以报告非致命性失败,此后当前测试将继续进行。因此,你可以在单次编辑-编译循环中检测并修复多个错误。
- 测试框架应该使编写测试的人摆脱杂务,让他们专注于测试内容。GoogleTest 会自动记录所有定义的测试,并且不需要用户列举它们就可以运行这些测试。
- 测试需要能快速运行。使用 GoogleTest,您可以跨测试重用共享资源,并且只需为设置/拆卸支付一次费用,而无需使测试相互依赖。
测试里的几个术语解释:
-
黑盒测试:又叫功能测试,关注被测软件的功能实现,而不是内部逻辑。在测试中,被测对象的内部结构,运作情况对测试人员是不可见的。
-
白盒测试:称为结构测试或者透明盒测试,要求测试人员对软件的内部结构和工作原理(例如 ABACUS 的密度泛函理论算法)有深入的了解,也迫使测试人员去思考算法的实现过程,可以检测代码的每条分支和路径,揭示隐藏在代码中的错误。单元测试属于白盒测试的范畴。白盒测试可以识别难以在黑盒测试里发现的问题。缺点是昂贵,且花时间。
-
灰盒测试:混合着白盒和黑盒的测试方法。最常见的灰盒测试是集成测试。
-
单元测试:单元测试(Unit Test)是对软件最基本组成单元(例如函数)进行的测试。这里的单元就是指软件设计的最小单位。单元测试分为两个步骤:
- 人工静态检查法:是测试的第一步,通过不执行代码而检查程序的逻辑算法,尽可能的发现问题。例如检查算法的前后逻辑,函数和类的接口是否合理,包括输入参数个数、顺序、类型,以及返回值类型是否一致;检查全局变量和全局类的使用是否必须;检查所用语法是否在不同编译器下通用。
- 动态执行跟踪法:执行测试案例来看是否有预期故障,包括预期的错误提示都可以编写单元测试来覆盖。
测试出问题分下列四种情况:
- 错误:可以执行,结果错误。
- 缺陷:可以执行,但是实际的结果和期望的结果有偏差。
- 失效:执行功能的能力丧失。
- 故障:不能执行,可能由错误,缺陷和失效导致。
ABACUS 中主要包含单元测试和完整性测试两种。其中单元测试确保每个单元函数的正确性,完整性测试确保一些功能能够顺利跑完,得到正确的结果。
测试案例(Test Case)是为检测程序的某个程序功能而准备的一组数据,包含测试的输入以及预期结果。随着测试案例数量的增加,开发者对产品质量和测试流程也就越有信心。除了对功能正确性的验证之外,建议也要考虑对程序不能运行,甚至错误的情况进行测试案例的编写,使得所有程序的行为都被测试所覆盖。
另外,我们积极的建议所有 ABACUS 的开发者采用“测试驱动开发”的方法来开发程序新功能,就是一个新功能在编写代码之前,就考虑清楚并且准备好测试案例来验证程序满足需求。