17|兼容性问题:如何有效发现兼容性问题?

久别重逢,我想先问你一个问题:实现一个基础平台,技术上最难的事情是啥?

面对这样的问题,相信你可以毫不犹豫列出许多难题,而且可能理由都非常充分。我给出的答案可能不太一样,我认为兼容性才是最难的

兼容性问题,与其他问题相比,多了一个时间维度。也就是说,难度再大的技术难题始终都是一时的,解决了就是解决了,时间维度可以无视,但兼容性问题却不是这样的。它难就难在它是一个随着时间递增的包袱,滚雪球似的,终有一天这个负担会越来越大,直至压垮一个团队(无论技术多强,无论人力多少)。

低代码平台当然也有兼容性负担,不但有,而且非常重。设想一下,平台团队合入了一个 change,导致昨天还好好的 App 今天就跑不动了,这种事情搁谁那都说不过去。从我推广低代码平台的经验来看,兼容性是劝退应用团队的一个重要因素之一,甚至与适用性问题并列。毫不夸张地说,能否妥善解决兼容性问题,决定了低代码平台是否能走得长远

要解决(或者规避)兼容性问题,可以分为两个主要步骤:发现和治理。今天这一讲我们先来聊聊如何发现兼容性问题,主要和你介绍下我正在用的三个方法,下一讲我们再从技术上聊聊如何有效治理兼容性问题。

代码走查

代码走查的作用有很多,再怎么强调都不为过,兼容性问题的发现,是代码走查的作用之一。

而代码走查机制的核心要素(没有之一)就是评审员,一名合规的评审员应该具备这样的素质:

  1. 深刻掌握被评审的子系统所用的语言、工具、第三方库等的各方面性质,对它们的长短处了如指掌;
  2. 深刻理解被评审子系统的全貌,大到架构、关键流程,细到各个功能的实现,最好是原创作者,次好是长期维护者;
  3. 最好是专职的,而不是临时抓来的人头,如果是临时参与评审,Leader 需要预留充分的时间,并计入业绩。

代码走查的另一个要素是要避免自我走查。不能让代码作者既当运动员又当裁判员,这很好理解,但并非所有执行代码走查的团队都能做到。

这一点其实是在说,要有一套管理机制(甚至是文化)来促使变更的代码能被切实有效地阅读和思考。这点展开会是一个很大的话题,而且还是一个管理层面的话题,不是我们专栏的关注点,有机会我们再在别的场合聊聊。

那为啥开发人员发现不了自己的代码带来的破坏性变更,反而是需要别人来发现呢?有一句古诗你肯定听过:横看成岭侧成峰,远近高低各不同。

资历再深的研发人员,在修改低代码平台代码的时候,总会陷入细节,往往会专注自己眼下的问题,而对所做修改可能会波及到的部分考虑不足,所谓一叶障目,不见森林。而代码变更所带来的影响,往往会在某个意想不到的角落产生破坏性变更(即不兼容修改)。这是人之常情,我们只能承认这是一个无法避免的事实。

如果说资深研发人员未考虑到他修改的波及是一种不该发生的无意失误,对于资历较浅的研发人员所做的代码修改的波及,则不能用失误来解释,而是限于对系统的肤浅理解,他们根本就无法预知他们所做的修改可能会产生哪些不良影响,这些不良影响中的一部分,会体现为系统的破坏性变更。

但专业评审员在走查代码时,不仅会关注所修改的代码本身,还会自然而然地把这份修改放到系统全貌的上下文来看,于是,许多代码作者看不到的波及,会很自然地被发现。

DevOps 流水线

代码走查是一种兼容性问题的主动发现手段,这个方法对评审员的经验和能力有强依赖。我们都知道,在软件研发领域,最不可靠的因素就是人的因素了,因此代码走查的方式是不可靠的,仅凭代码走查手段也是难以发现所有兼容性问题的。

DevOps 流水线是提升软件研发效能的一个非常重要,甚至是必要的手段。它主要采用形式多样的自动化检查脚本,来发现软件研发过程中包括兼容性问题在内的各种问题,从而提升软件研发效能和质量。相对代码走查来说,DevOps 是一种被动式的兼容性问题发现手段。

我们可以在 DevOps 流水线上加入各种专门用于发现兼容性问题的脚本,在代码走查之前就把大多数低级的、已知的兼容性问题找出来。评审员则利用其知识和对系统的深刻理解,发现深层次的兼容性问题。

值得提示的是,在评审员发现了某个兼容问题之后,我们可以进一步思考是否可以将这个问题的发现过程脚本化。一旦脚本化,我们基本就可以杜绝相同问题再发生了,可以点滴提升流水线的可靠性。毕竟,同样的问题,评审员下次有可能看走眼,但脚本则不会。

那么,具体有哪些切实可行的兼容问题检测方法可以植入到 DevOps 流水线里呢?

首先是 UT/FT。

理论上,如果每个函数每个功能特性的输入输出在代码修改前后都能保持一致,我们就可以认为系统是完全兼容的。但这只是理论,在实操过程中,代码行、函数(类),条件分支的覆盖率不可能做到 100%,但我们起码可以确保有 UT/FT 用例覆盖的那部分是能向后兼容的

UT/FT 成本其实挺高的,为控制成本,我们采用的是这样的做法:

  • 新模块新功能在初创时,评审员会要求各个功能点都要有一定比例的测试用例代码;
  • 功能特性在后续迭代过程中,如果在某个函数上踩到坑了,我们就会针对这个函数补充对应的用例代码,避免再次踩坑。

这是一个比较经济的方法,在不需要很高的覆盖率前提下,让用例代码最大发挥价值,你可以借鉴一下。

其次是各种代码扫描工具。

代码扫描工具可以很好地弥补 UT/FT 用例覆盖率不足留下的空白地带。虽然这些代码漏洞扫描工具一般都集成了数量不少的扫描规则,被这些规则命中的代码不一定都有问题,但出问题的概率会大幅增加,所以引入并重视这些漏洞扫描工具的报告,尽可能地将报告里的所有潜在问题项都改掉,也可以避免一定数量的兼容性问题。

需要预警一下的是,这些漏洞扫描工具往往是收费的,而且扫描工具的安装比较费事(虽然厂家会提供技术支持),扫描过程耗时耗 CPU。另外你第一次看扫描报告的时候,要先有心理准备,数以千计的乃至上万条漏洞报告很容易让你对自己的编码技术产生怀疑。

最后是静态检查。

看到这个点,也许你会马上想到各种 lint 工具,但我们团队并没有用。lint 工具确实可以减少一定的工作,但凡事都有代价,我认为使用现成的 lint 工具会被它的能力给框住,再考虑到全员的学习成本,实际是得不偿失的。

根据我们的经验,兼容性问题有一个特点,就是你永远无法预知它会在哪儿、以哪种方式出现。在代码走查的部分我说了,我们习惯了将兼容性问题的排查过程脚本化,兼容性问题的特点也就导致了检测脚本必须有多种不同的形式。

我们的静态检查脚本的形式可谓是不拘一格,有配置文件一致性检查,也有用正则暴力对源码进行检查的,正则搞不定时,还用 AST 解析深度检查的。能用 shell 脚本完成检查,就不会用语言,shell 搞不定或太慢的,再用其他方法(一般是 node.js)编写脚本来检查。一般的 lint 工具做不到这么灵活。

尽量将已知的兼容性问题脚本化是很重要的,这样可以避免在同一个坑里掉两次。这一点不仅是为了减少兼容性问题的发生,更主要的是协作方面的意义。一个问题第一次出现时,应用团队往往会抱以理解的态度,但如果同一个问题一而再、再而三地出现,对方就会很不耐烦了。

利用实际用户数据

先说明一下,这里的用户数据就是实际的 App 工程配置数据。在 UT/FT,或者 ST 时,构造被测数据往往是一个非常麻烦且令人头疼的过程,而且构造出来的被测数据往往不真实或者太简单,测不出问题来。那我们为啥不直接用户数据来测试呢?而且,对于低代码平台来说,用户数据天然就受到平台的管理,从库里复制一点数据来测试,简直不要太容易。

复制数据容易,但要找到质量较高的被测数据,则需要花一番功夫。

在我们的系统里,有许多 App 工程数据是开发人员的实验性工程(我估计这是一个普遍现象),这些工程是没有验证的价值的,需要排除掉。我采用了两个条件来排除这些工程。

第一个是根据基础指标来筛选,我采用了工程的编辑次数,模块数量等两个维度作为基础指标。根据经验,我设定了编辑次数大于 300 次,且模块数量至少为 2 的条件,作为筛选指标。你可以根据你的实际情况适当调整数值,或者按需增减其他维度。

不过,根据基础指标筛选到的 App 工程数据,仍然可能包含有问题的数据,此时我们可以引入第二个条件,来对它们进行更加细致的检验。我增加了低代码编译器的检验,只有能通过低代码编译器的编译的 App 工程数据才能进入下一个环节,用来校验新的修改是否有破坏性变更。

对于那些对没有更改生成的 App 代码的修改,我们可以直接使用筛选后的 App 工程数据进行校验。方法很简单,就是用修改前的编译器编译生成的代码与修改后的编译器生成的代码做比较,如果有修改,则表示可能会有问题,应该由专家评估是否是有破坏性变更。

对于那些会修改 App 代码的变更,则会麻烦许多。虽然也可以采用比较源码的方式来比对,但会比较麻烦,甚至需要将生成的代码解析为 AST(抽象语法树)之后再一一遍历比较,测试用例的开发难度较大、可维护性不好,所以我们团队没有采用。

我们使用的是 UI 自动化测试的方法,也就是把 App 直接跑起来,然后自动操作 UI 到某个状态,最后截图比较修改前后的差异。比较截图时,不是严格按照 100% 比较的,而是设定了一个 90% 相似的阈值,低于这个阈值就认为 UI 不匹配,从而报错。一般来说,一旦出现兼容性问题,UI 的差异会非常大,比如取不到数据,或者状态压根就打不开等。

这里需要补充一点儿背景,不然你很可能会觉得,采用 UI 自动化测试的方式难度更大,甚至压根就不可行。

这个背景就是我们的低代码平台 Awade 提供了自动生成 App 的 UI 自动化测试代码的能力(专栏的后续部分我会给出具体如何实现),只需开发人员配置待测功能点,以及预期数据即可。根据这些信息以及 App 工程数据的其他信息,就可以生成出 UI 自动化测试用例代码了。

在做兼容性测试的时候,我会先找有做过功能点自动化测试的 App,然后将这些 App 的自动化测试用例跑起来,然后在关键节点抓图比对。即使 App 所配置的 UI 自动化测试是错误的也没关系,因为只要待测修改的代码也能触发相同的错误就行啦。

虽然你现在不一定有条件利用 UI 自动化测试的方法来做兼容性测试,但至少可以对编译器做测试。即使是只做到对编译器的测试,只要你利用实际用户数据来测试,就能发现许多兼容性问题了,并且这些问题都是其他方式发现不了的。人工走查的方式不可能做到如此细致,也无法替代基于已有 App 工程数据测试所发现的兼容性问题。

小结

这一讲我介绍了多种我正在用的发现兼容性问题的方法,这些方法多数可以被直接借鉴。我们之所以要无所不用其极地从各个角度来发现兼容问题,是因为兼容性问题是一种比任何功能性问题(如功能缺失、bug 等)都更加麻烦的问题。

一方面,兼容性问题是一个低代码平台所有问题中最为劝退的一种问题。兼容性问题会给应用团队带来额外的对齐工作,而且这些工作往往是应用团队计划外的、被动式的工作。即使应用团队没有做任何修改(哪怕他们的 App 已经冻结版本),但是为了能够正常运行,他们不得不花额外的资源来对齐,这样的次数如果很多,他们必将萌生退意。

另一方面,没有被及时发现和处理的兼容性问题,会让平台团队陷入一种奇特的“怎么改都是错的”的境地,即使是那些非致命的兼容性问题,都会引起这样的后果。比如我曾经处理过一个 UI 安全边距的问题,就让我陷入了这样的境地。

某天我收到一个 bug:一个 App 的安全边距消失了。我们很快就查到是大概两个月前的一个修改导致了平台生成的代码没有自动加上安全边距,这就是一个典型的非致命兼容性问题。你有没有发现,此时此刻,无论我改掉这个 bug 还是不改,都会“得罪”一些人:恢复安全边距的话,这两个月里新增的 App 都将出现安全边距过大的问题;不恢复安全边距的话,两个月前创建的 App 都将没有安全边距。

之所以需要从不同角度切入来发现兼容问题,就和常见的一次性医用口罩的生产是一个道理。普通的纺织布是不能用于生产一次性医用口罩的,因为纺织布是由经线纬线规规矩矩交织而成的,两根线之间会更容易留下缝隙,病毒会很容易从这个缝隙进入口腔。而熔喷布则是把塑料融化后乱七八糟喷在基材上,只要满足一定的厚度要求,就不会留下缝隙了。同理,我们必须从各种差异很大的角度切入,采用差异很大的方法,尽可能多地发现兼容性问题。

但兼容性问题的发现是解决的第一步,下一讲我会详细介绍如何妥善处理兼容性问题。

思考题

相信你应该会有简单或复杂的 DevOps 流水线辅助你的日常开发,你现在正在跑的流水线任务里,有哪些任务是可以协助你发现兼容问题的?有哪些是稍加改造就可以用于发现兼容问题的呢?

欢迎在评论区留下你的看法。下一讲你不会等待那么久,我会尽快更新。