程序员究竟干不干测试

对于程序员来说,我们的第一反应是“测试软件功能应该是 QA 干的活”,实际上我们平时的软件开发中有大量的时间,就是在干“测试”。

所谓的”测试“,可以分为两个部分,一部分是验证功能有没有问题,在我们平时的开发流程中,多数表现为,“写一段代码,就跑一下打开控制台或者浏览器简单操作下,看看有啥问题”,另一部分是出现问题时进行定位,”比如打开浏览器控制台进行 debug 操作“。

可以说,我们只要在开发,其实就是在“测试”与“编码”之间无限循环,“测试”是如此重要,重要到它贯穿了我们整个软件开发过程。我们需要非常高效地实现它们。那么“无计划的手动验证”与“手动的启发式定位”都是无法容忍的低效手段,必须将它们替换为“有计划的自动化验证测试”和“有计划的逐模块自动化排查”。TDD 就是从手动到自动的手段。

最近有一个很火的概念叫 vibe coding,基本流程是:

  1. 给 ai 一个需求,让其完成(之前是手动写)
  2. 按需 accept 变更后,直接执行
  3. 把控制台中的报错信息重新告诉 ai 并修复(之前是手动 debug)
  4. 循环往复进行下一个需求

这个流程看上去是自动化,其实它只是更高频次的手动验证与定位,ai 并不能理解 bug 产生的原因,仅仅是按照控制台信息,去毫无计划不断套用可能的方案。这和我们平时在那些令人头大的遗留代码中解决问题,需要时不时改几行试着看看做的事情是一样的。即然我们平时在这种开发模式下屎上堆屎,造成大量预期外的问题,我们自然也不能指望 ai 在同样的开发模式下,能有多好的质量。

TDD 究竟干的是什么

现在我们有这么一个需求,我给你字符串“SUM(1,2)“,需要解析这个字符串并计算出结果为 3。现在我们不知道怎么写,中间要如何解析,有什么其它复杂场景也不知道。我们可以通过一个 TDD 做一个简单的 spike,进行快速的设想验证。那么最快速的案例就是下面这样。当然了,下面的测试肯定是不会通过的。

describe('spike', () => {
  it('should ', () => {
    expect(new Calculator().calculate('SUM(1,2)')).toBe(3)
  });
})
 
class Calculator {
  calculate(formula: string) {
    return formula
  }
}

没关系,我们现在用最简单的方式让它通过,直接硬编码就好,反正是 spike,后面都得删掉。

describe('spike', () => {
  it('should ', () => {
    expect(new Calculator().calculate('SUM(1,2)')).toBe(3)
  });
})
 
class Calculator {
  private readonly functions = new Map([['SUM', (a: number, b: number) => a + b]])
 
  calculate(formula: string) {
    const functionName = 'SUM'
    const currentFunction = this.functions.get(functionName)
    return currentFunction ? currentFunction(1, 2) : 0
  }
}

在 calculate 代码中,我们可以知道,我们其实要做的有三件事

  1. 从字符串中找到函数名 SUM。
  2. 根据函数名称找到对应的函数计算逻辑,找不到的话还要有一个兜底机制。
  3. 传入对应的参数,计算出最终的结果。

能够想起来编译原理知识的人,就知道这里首先要做一个词法分析器,想不起编译原理也没关系。我们的工作重点,天然的会从能根据公式计算出结果,聚焦能对公式进行正确的解析。然后我们的根据上面的信息,细化出一个详细一点的任务列表:

  1. 实现 Lexer a. 应该从 SUM(1,2) 中,解析出类型为”FUNCTION”的字段“SUM”; b. 应该从 SUM(1,2) 中,解析出类型为”LEFT_BRACTED”的字段“(”; c. 应该从 SUM(1,2) 中,解析出类型为”NUMBER”的字段“1” 和 “2”; d. 应该从 SUM(1,2) 中,解析出类型为”COMMA”的字段“,”; b. 应该从 SUM(1,2) 中,解析出类型为”RIGHT_BRACTED”的字段“(”; e. 如果”()“不能成对,则抛出错误 f. 如果解析出来的类型,不属于以上任何类型,则抛出错误
  2. 获取函数 a. 应该从函数列表中,获取 SUM 对应的函数 b. 如果找不到 SUM 对应的函数,应该抛出错误
  3. 函数计算 a. 如果 SUM 函数中,有一个参数不是数字,那么就应该抛出错误(在 1.f 中已经抛出错误了,其实这里根本不会走到,以后如果有新的字段类型的时候再处理即可)

从上面可以看到,TDD 不是让开发人员“浪费时间去写测试”、“把测试人员替代掉”,而是在正确理解任务背景的情况下,将任务划分为一个个可验证的功能里程碑,同时,借助测试文件将我们平时那些低效的测试手段,以可重复的(ai 有幻觉是不可重复的)、高效的自动化流程代替。虽说 TDD 的全程是 Test-Driven Development,但我一般称其为 Task-Driven Development。Task is Test。编写测试文件的时间成本反而不是 TDD 无法推行的主要原因,而是任务分解本身就没做好。虽然我们都讨厌“面试造火箭,工作拧螺丝”。但更多的现状是,不仅火箭造不出,螺丝也的有问题。

那么能不能直接在一开始就让 ai 进行初始的任务分解呢,先不论 ai 对复杂任务的分解能力如何,在完全直接寻求 ai 帮助下,往往会使我们对 ai 的结果无所适从。

回归真实的业务

直接寻求 ai 帮助,但是没有合理判断 ai 结果,就直接应用 ai 结果,应该是我们平时使用 ai 最常见的场景。在没有 ai 的场景下,最贴近的场景就是“给人 review”“只看语法问题不看背景""来不及了改了,代码上了再说”。

这里的无法合理判断,其实是没有一个团队共识的架构模式,不知道有多少团队,团队中随便找几个人,对同一个任务,可以有各种各样的任务分解方式,连最基本的调用哪些组件,按什么顺序分层