乔梁在MTSC大会上分享了主题为《一致性是研效提升的必经之路》的演讲,演讲内容主要是如何提高软件研发效率。他从香农在信息论中的熵增定律出发,讲到像软件这样的复杂系统,必须尽可能减少不确定性因素的影响,努力提高软件研发的一致性,从而达到提高软件研发效率的目的。 本文主要基于熵增定律,探讨该定律对软件测试行业是否存在指导作用。
首先,我们回顾一下什么是熵增定律。 这原本是热力学中的概念,一个孤立系统总是趋向于熵增,最终达到熵的最大状态,也就是系统的最混乱无序状态。1948年,香农将热力学的熵,引入到信息论。对于一个信息系统,如果没有外力作用,最终也会达到信息熵最大的状态,信息系统会越来越混乱,不确定性会越来越强。
影响软件系统运行的因素非常多,每个因素之间都会相互影响,所以根本无法根据单一因素来预测软件最终的状态。这和股市很相似,你最好永远不要通过某个单一信息预测股市的走势,因为单一因素和最终的结果不是线性相关的。
一个软件写好了之后,你直接部署好,过了一段时间再去看,它可能就不是你预期的样子了,可能早已经报了网关错误, 可能页面展示 500 错误,或者干脆无法访问了。 有很多原因都有可能造成这种混乱的状态,比如某个用户的操作触发了一个隐藏很深的缺陷,对程序造成了一个很小的修改,之后其他用户再进行了一些其他的操作, 也有可能是遭受恶意攻击等, 当然还有可能是你的服务器到期没续费,或者是云服务商倒闭了。
所以就算你的软件已经接近完美,不再需要任何修改和更新,你可以很长时间不去维护,软件也会变得越来越混乱。 而在一个产品的实际研发过程中,我们还需要不停的为软件系统添加新功能,增加新特性,带来更多信息(比特),导致系统更加复杂,这些新信息会进一步增加软件的信息熵。
一个软件系统越复杂,影响的因素越多,就越难通过人力控制。几乎所有的公司都想利用已经成熟的技术体系提高研发效率,但是实际上研发效率只会越来越低,一个产品往往是初创阶段研发效率高,越到后面效率越低。 原因之一就是因为业务增长必然导致信息熵增加,不确定性更高,干扰因素更多,从而拖慢研发进程,产生更多 bug。
在信息论中,信息熵通常用概率的倒数表示。概率越低表示程序的不确定性越高,信息熵越高,随机性事件就更容易形成 bug。
第一种因素是依赖的环境不确定。比如操作系统发生变化、数据库品牌和版本发生变化、访问的文件权限发生变化。
第二种因素是状态管理。可能发生变化的任何数据都处于某种状态,比如程序中的变量、数据库中存储的数据、文件中的数据。 这些数据可能被同一个系统的不同阶段读取和修改,但是外部根本不知道他什么时候修改的,从而造成不确定性。 漫游测试中的快递测试法采取跟踪数据,查看系统中哪些特性会读取和修改这个数据,从而测试这种不确定性。
第三种因素是程序的分支路径。如果你没有一个非常完备的定位系统,当你走在一条满是岔路的主干道上,你一定会迷路。
对于一个复杂的软件系统来说,熵增几乎是必然的,再加上加入越来越多的新功能(信息熵),软件的质量也会越来越无法保障, 我们能做的,只有投入越来越高的成本,延缓它的混乱状态形成过程。 我们也可以利用这种混乱状态的形成原因,改善自己的工作,就像你虽然无法预测股市的走势,但照样可以从中赚钱一样。所有的不确定性系统都没有办法通过单一因素进行控制,因为你不知道你控制的因素会不会对其他因素产生影响。所以你应该放弃对不确定性系统的控制,让他自由发展,我们要做的事情就是:计算不确定性,计算概率。
比如你计算出某只股票上涨的概率有 100%,那就代表这世界上没有哪个因素会阻止它下跌,这显然是不可能的。 你可以自己列举影响股价走势的因素,然后分配权重,最终计算出概率。需要注意的是,你永远不可能列出所有的因素,但你也不用慌,你可以把没有考虑到的其他因素放到一起,叫其他,也分配一个权重。 在一次一次的操作中,你会发现你的模型不够好,计算出的概率不精准,当你发现了其他因素,再修改和调整模型。
开发软件也好、测试软件也好,和买卖股票并没有什么区别,还是计算不确定性,你需要计算哪些随机事件会对系统产生影响。然后设法提高确定性和一致性。
现在我们写一个很简单的程序:
def run(): print("hello world")
这是一个信息熵很低的程序,确定性高,每次运行这个程序几乎总是得到一个确定的结果:在显示设备上打印 hello world。 但你永远不能说它是确定的,发生的概率是 100%。 如果程序依赖的环境变化了呢?比如某个新的版本不再支持这个 print 函数了呢?比如 print 的用法发生变化了呢? 比如你的操作系统不能正确安装? 比如你的显示设备无法正常工作?
幸运的是通过 DevOps 研发体系,我们可以使用 docker 这样的确定性工具来部署,尽量保证依赖环境的一致性,因此在测试时我们可以减少环境不确定性带来的权重,但是并不表示我们不需要测试环境本身。 比如用户选择什么样的客户端、客户端版本都是我们不能确定的,那就不要对不同的浏览器,不同的app版本进行测试。(当然程序也可以选择强制规定用户只能选择某个客户端)
对于系统、数据库、客户端、版本号、解释器等所有的依赖最好纳入缺陷分析的范围中。
接下来是用户输入造成的不确定性。任何一个程序,用户的输入是不受控制的,这就会对程序造成非常大的随机性的影响。
def run(user_input): print(user_input)
print 函数能处理所有的用户输入吗? 字符串可以?整数可以? emoji 可以?100G的数据也可以? 如果程序里面调用了其他程序 do_something(user_input), 这个程序可以处理所有的用户输入吗?
def run(user_input): do_something(user_input)
在 java 这样的静态类型语言上,已经对用户输入的参数类型从语言层面做了类型检测,提供了标准化和一致性的方法, 而像 python 这样的动态类型语言则必须由开发人员自己进行校验,程序的确定性保障更多的需要依靠开发人员的逻辑能力和编码水平,而不同开发人员会形成不同的处理方法,增加了程序的不确定性。
虽然静态语言对数据类型进行了类型检测,提供了一致性方法,但是不代表研发人员就可以放任用户数据不管。因为用户输入可以通过其他途径,比如输入长度超过处理能力,比如输入恶意数据破坏程序等等。
总之,开发需要进一步控制用户输入,就会编写分支语句和异常处理语句对程序进行控制,就好像为了方便所有的居民出行,一定会在主干道上挖出千奇百怪的小路一样。还是那句话,分支路径多了,人就容易迷路。
def run(user_input): if user_input > 10: ... elif user_input > 100: ..、 if isinstance(userinput, str): ... try: do_something(user_input) except ValueError: ...
对于完全无法确定的随机用户数据,程序会编写分支语句来形成确定性的执行,这多个分支是否运行就是概率问题。 测试程序时就必须考虑,开发人员是否考虑了所有的情况,如果某个用户输入没有对应的分支执行怎么办? 如果开发人员的分支处理不合理怎么办?
在编写代码的时候,if 语句忘记处理 else,分支覆盖不全的情况比比皆是。 异常捕获语句则只考虑了自己能力范围内的异常类型,有大量未知异常没有按预期方式处理。 开发人员还会使用大量第三方库进行参数验证,简化自己的参数验证代码,但是这些第三库是否符合要求,有没有漏洞和bug,是否按正确方式使用了,又是个不确定性问题。
除了用户的输入参数,在程序运行过程中,还会使用其他的数据,比如系统生成的时间戳、随机生成的变量,使用了经由其他程序修改后的数据等等。 这些数据都不是程序员自己能控制的,属于随机性事件,因而很难被发现、控制和测试。
对于开发人员来说,尽量编写确定性高的代码,能有效抑制 bug 的产生。什么是确定性高的代码呢?比如编写纯函数,尽量减少对其他接口的依赖、不随意修改全局变量、不随意修改类中的属性, 尽量减少对文件、数据库等状态数据的依赖。(这样的代码我叫做可测试性高,但是有几家公司是这样写代码的。)
对于测试人员来说,则需要分析哪些特性适合编写纯函数、哪些地方的数据是系统生成的、分析用户输入会形成哪些分支代码、分析依赖了哪些数据库数据、文件数据、以及其他接口提供的数据,同时,测试人员自己编写的代码也应该参考这些情况,减少不确定性。
最后,多个程序之间、多个特性之间、多个模块之间相互调用形成的不确定性更加难以控制。 功能越复杂,涉及到的数据会越多, 根据不同的操作顺序,形成的排列组合会成指数级上升。 使用场景测试法、判定表等方案从理论上是可行的,但是实际测试过程中很难全部覆盖到。 目前通过像智能化测试、精准测试这样的技术也许能缓解部分测试难题,但是这些技术手段本身的信息熵就很高。
乔梁还从管理角度提出了以下一些提高一致性、减少不确定性的解决方案,可以作为参考。
流程:同一流程,不同人执行的产出结果与质量是否一致?
工具:同一件任务,内部有多少种工具平台?
个人:同一产品代码,在多大程度上像是同一个人写出来的?
组织:在结构与机制上,是否支持员工追求“一致性”?
你已经阅读完本文所有内容。相信你一定是个耐心和踏实的人,也是一个可交的朋友。如果你有兴趣,可以复制 『 jiubing1 』或者点击下方名片加我为好友,一起学习和进步。