27|围绕测试工序的认知对齐

你好,我是徐昊,今天我们来继续学习 AI 时代的软件工程。

上一节课,我们介绍了认知分歧,以及在软件开发过程中,理想的认知行为模式是什么样的。

理想的认知行为模式,应该看作是团队认知行为的基线。也就是只有达到了这个基线之后,才能看作是胜任的团队成员。那么怎么才能有效提升团队成员的认知呢?今天我们就来讨论这一话题。

最低胜任要求

对于团队中人数最多的开发者而言,拉齐认知基线最简单的方式,就是通过测试工序。

正如我们前面课程中介绍的,在软件开发的过程中,我们按照测试策略的指引,逐步完成架构中不同组件的开发与集成。测试策略指定了我们需要完成的任务。而通过架构与测试策略分解构成的测试工序,定义了开发过程中不同的任务种类。在给定的架构与测试策略下,测试工序的数量是有限的。

对于每一个开发者而言,他们所需要掌握的技能就只有,按照测试工序分解任务,以及准确地按照测试工序指引,编写测试和生产代码。我们对于开发者的胜任要求,也就变成了,是否能够准确地拆分任务,以及是否能够掌握每一个测试工序。

我们不难发现,按照测试工序拆分任务属于复杂认知行为模式,而掌握每一个测试工序,则会因工序的要求,处于不同的认知模式。我通常的建议是,可以先从掌握每一个测试工序开始。

此时,团队需要围绕每一个测试工序,构造一些典型场景,让新加入的团队成员按照典型场景练习测试工序的应用。在前面的课程中,我们给出过这样一个样例工序:

  1. 使用 Fake 数据库,测试 Persistent 层中的组件;

  2. 通过 Stub Persistent 层的组件,测试 Application Logic 层中的组件;

  3. 通过 Stub Application Logic 层的组件,测试 HTTP Interface 层中的组件;

  4. 使用独立的测试数据库,对三层组件进行功能测试。

我们围绕这四个工序,给出典型的场景。比如,对于持久化层,典型场景就包含如何构造 fake 数据库、如何灌注测试数据以及如何将 fake 数据库接入持久化层。而对于 HTTP interface 层,则会包含如何构造 HTTP Client、如何 stub application logic 以配合测试上下文等内容。

这样开发人员在加入团队的时候,就能够有的放矢地学习架构、测试策略以及团队中确立的最佳实践。而站在 Tech lead 或者 team lead 的角度,我们也能准确地知道,每一位成员是否在团队中是胜任的。掌握了全部的工序就是胜任的,不能掌握则是不能胜任的。如果开发不能掌握所有的测试工序,那么会带来质量缺陷和效率下降。因而最低限度的胜任是可以掌握所有的测试工序。

这么做的另一个好处是,我们对于胜任有了明确的定义和预期。可以更有效地利用时间,特别是对于加入团队的新人而言,是一种更友好的方式。

拉齐任务拆分的认知分歧

除了掌握每一个测试工序之外,准确地拆分任务是更重要的一项能力,是更难传递的不可言说知识,也是最容易出现认知分歧的地方。有这么几种做法,可以有效地传递这个不可言说知识,拉齐认知分歧。

一个是结对编程(Pair Programming)。结对编程可以促进实时讨论和交流,从而使得不可言说知识更容易被理解和吸收。一名程序员可以向另一名程序员解释他的思维过程、选择和决策背后的逻辑,从而使得不可言说知识得以更清晰地传达。

结对编程还可以通过反馈机制来加深程序员对不可言说知识的理解和掌握。当一名程序员使用不可言说知识时,另一名程序员可以提出问题、提供反馈和建议,从而帮助他们更好地理解和应用这些知识。最终有效地拉齐认知分歧。

当然,在现实工作中,结对编程是较为少见的。一个退而求其次的做法是,在开发工作开始之前,全组开发一起做任务拆分。可以每人负责一张故事卡,或是划分成小组,每个小组负责一张故事卡。每个小组按照测试工序拆分完成之后,向全组人讲解他们拆分的结果和过程,接受其他人的反馈的建议。

在全组达成共识之后,可以写下任务列表,这样无论后续是谁接手开发,都可以按照拆分之后的结果继续。如果在拆分的过程中存在分歧,那么小组也可以充分讨论,快速拉齐认知分歧。

对于一个熟练使用测试工序的团队而言,差不多两三个小时,可以完成整个迭代的任务拆分。在这个过程中,也可以重点照顾新加入的成员,让他们主导拆分的过程。团队中熟练的成员给予指导和建议即可。

另一个可行的方式是在代码审查(Code Review)中增加任务审查(Task Review)环节。与代码审查类似,任务审查也是一组人一起,对彼此的工作进行互评。所不同的是,后者关注点并不在最后的代码,而是拆分的任务。

每个人先展示自己拆分的任务列表,然后再展示自己按照任务列表实现的代码。其他人先针对任务列表给予反馈,然后再对代码提出意见。这么做除了反馈任务拆分能力之外,也可以检查测试工序的执行程度。

此时的关注点在任务的划分与工序实施的准确度,至于代码好坏反而并不是太重要。或者换句话说,比起代码质量,我们更关注的是是否存在认知分歧。从长远来看,消除了认知分歧,能带来更好的质量和更高的效率。

说句题外话,代码审查在我看来是最没用的实践之一。当出现坏味道的代码时,重点并不是改掉代码,而是要理解为什么会写出这样的代码。而答案通常都是对于需求、架构或测试策略的误解。不消除这些误解,不好的代码还会源源不断地出现。

按精益理论来讲,就是比起修复缺陷重要的是修复人。这恰恰是代码审查时容易被忽略的部分,引入任务检查就好了很多,让我们更多地关注在思路的对齐,而非简单的结果校验。

围绕工序的回顾

前面我们讲到的实践,主要关注在如何有效地引入新人,降低新人的认知分歧。

然而项目并不是一成不变的,随着项目的深入,还会有新的知识被提取出来。特别是随着项目的发展,可能会引入新的组件,带来架构上的改变。也可能会引入新的工具,带来测试策略的改变。

这些改变很多时候是不易察觉的,比如,某一次重构中,引入了新的组件。或是因为简化某些步骤,引入了其他的工具。在当时看来可能是微不足道的改变,但是最终都会带来测试工序的改变。没有在团队中拉齐的测试工序,就会引入认知分歧。那么阶段性、周期性地回顾工序,发现并消除认知分歧就非常重要了。

一个有效的办法是,围绕测试工序进行专门的回顾会议,重新列出所有测试工序,询问目前是否有改变,或是针对性地讨论是否要对目前的测试工序进行调整和修改。如果是刻意的架构调整或是重构,那么可以重新拆解新的测试工序,并与旧的进行对比分析。这样的回顾不需要过于频繁,通常每个迭代一次即可。

小结

围绕测试工序还有很多做法,比如按照测试工序建立效能基线,这是因为测试工序表示了不同的任务种类,而同类任务具有一定的相似性。那么以测试工序为基准的效能基线,就能更准确地发现效能的瓶颈。再比如,可以使用测试工序作为筛选供应商或是招聘的门槛,这样就能有针对性地挑选符合项目实际需要的人员。

测试工序是我们构建团队以及提升认知的重要抓手。它之所以能发挥如此重要的作用,在于它将重要的知识(软件架构和测试策略)凝练成易于应用的形式。

思考题

如何使用 LLM 帮助我们做工序回顾?

欢迎在留言区分享你的想法,我会让编辑置顶一些优质回答供大家学习讨论。