2021-08-25

代码的正确性

最近在开发 Spacemesh 的矿池程序,整套核心的代码现在已经完成了将近 70%。为了能保证所有的代码的正确性,使用测试驱动开发这种方式将各模块都使用不同的测试用例进行覆盖,以此来最大程度的保证写出来的代码的正确性和有效性。于是整个开发的过程就是在不断的验证和调试,直到所有的测试用例通过,最终将项目推进到接近完成的状态。测试,日志和调试,都是为了保证程序最终执行结果正确的重要手段。

测试

当需要开发一个稍微有一些些规模的项目,经常性的会陷入到对程序是否能够被正确执行的怀疑中,不管人有多聪明,记忆力有多好,当项目增长到一定的规模时,都会掉入这个陷阱中。使用测试驱动开发,可以说是一种非常好的针对这个问题的解决办法。

所谓的测试驱动,是指我们先写好一个功能的基本接口,添加针对该接口的测试用例,然后继续接口的实现的开发,直到所有的测试用例通过测试,步步为营,最终完成。测试用例似乎成为整个开发的主角,而涉及业务代码,却是保证测试用例正确性的桥梁。

使用了GoogleTest作为测试框架的测试程序运行结果

程序开发是一种组合方式,由各种大小部件组合成一个更大的部件,每个不同的部件都有各自的运行逻辑,测试用例则是为了保证某个部件的逻辑的正确性而存在的。

举个简单的例子:我们有一个函数,它可以把两个数字相加,然后把结果返回。于是,针对这个逻辑,我们写一段程序来验证,是否能够返回我们真正想要的值,这段程序就是一个简单的测试用例。

首先我们假定函数的接口是int Add(int a, int b),于是我们测试程序就可以写为EXPECT_EQ(Add(10, 20), 30)EXPECT_EQ是 GoogleTest 中的一个宏,它假定给入的两个参数的值是相等的,如果不等,它会打印出测试失败的信息,如果相等则表示测试通过。使用不同的测试框架,测试的代码不尽相同,可以查阅对应的测试框架的文档。我们可以为这个做加法的函数写很多个测试用例,来测试各种情况,比如带入负数相加,其中一个参数是零,或两个参数都是零,然后验证是否能获得想要的结果等。

日志

日志就是在程序运行中,打印到屏幕或文件中的一些和当前程序运行相关的信息。程序日志是一种必不可少的记录工具,它会在程序最初运行就开始将程序状态记录下来输出至指定的设备,当程序出现错误甚至宕机时,开发员可以从这些日志里分析出程序错误原因直至修补错误。日志类型有很多种,常见的有:Debug, Info, Warning, Error, Critical。

一开始我对于这些日志的类型是有点模糊不清的,后来在 Stackoverflow 上专门查阅了一些关于日志分类的说明后,大致可以这样理解:

  • Debug – 输出一些帮助调试的信息,没有特殊要求的情况下,该类型的日志不显示
  • Info – 输入当前程序的状态,比如收到了什么信息,然后正在进行什么操作等
  • Warning – 当一些操作有可能导至程序错误时,使用这种类型的日志来记录这些操作,作为警告
  • Error – 当程序发生错误时,这些错误还没有大到会导致程序运行失败的结果,但是已经是作为错误发生了,有可能需要针对它进行程序修补
  • Critical – 有时候这种类型会被称作Fatal,出现这种类型的日志时,往往程序已经无法再继续下去,该日志记录导致无法继续的原因

合理的在程序中的不同地方嵌入日志,可以让开发者更加容易的找到程序中存在的问题并且定位问题位置直到解决。在写代码的过程中,有意的培养自己对于代码日志编写的规范和合理,是一件值得投入精力去做的事情。

调试

当程序出现问题时,或和预期出现不一至的表现时,比如某个测试用例没有通过,我们就需要找到错误的原因然后对程序进行修改,然后将这个问题解决掉,这个过程就是调试的过程。

调试的手段会有很多种,比如:使用调试器来挂载程序,然后设置断点来进行跳转到有可能出现问题的代码处进行单步跟踪和运行,在过程中查看相关的变量,判断程序走向是否正常。或者,通过查看程序的日志来分析和尝试定位问题的位置等。

关于调试,Linux 的作者 Linus Torvalds 就在一篇内核讨论的信中表示过,他完全不会用任何的调试器来调试代码,因为他认为,当一个你完全没有头绪的 bug 出现时,你要么变得更加的小心,要么去抱怨调试器(Oh. And sure, when things crash and you fsck and you didn’t even get a clue about what went wrong, you get frustrated. Tough. There are two kinds of reactions to that: you start being careful, or you start whining about a kernel debugger.)。其实我是很认同他的这一个观点,甚至不觉得这个观点有什么偏激之处。当我们太依赖调试器的时候,特别在碰到一些奇怪问题时,往往调试器展示给你的结果和实际的问题差得很远,而且一些在多线程中的宕机问题,往往调试器无法很好的帮助你去解决问题。

代码的正确性

关于如何保证代码的正确性,就是永远清楚自己在做什么,小心翼翼的写下每一个逻辑,做好每一个测试用例和日志。不要做多余的工作,不要把代码写得不必要的华丽,永远都要保持简单,并且多件事情分割成不同的小模块。写必要的注释,方便未来的人阅读,而这个未来的人往往就是自己,所以善待自己从写高质量的代码开始。

事实上,测试用例虽然和最终交付的代码看似没有什么直接关系,但是一堆好的测试用例,会帮助开发者省去大量的调试工作,并且帮助他们更好的理解这些代码。而好的日志,将会在程序的生命周期中不间断的产生有效的信息来帮助开发者们理解并处理未来将要发生的问题。

以上三大手段很有效的减轻了开发过程中的心智负担,但是,如何把这三大手段用对用好,是一个需要长期练习的过程。

MATTHEW
桂ICP备17005075号