一个完美的开发过程是这样的:测试先行,开发人员会些设计一些边界场景的测试用例,比如数据的取值范围从极大到极小、循环语句超出限制范围等等许多极端情况。这些测试代码会作为产品代码的一部分,以自检代码或者单元测试代码的形式与功能代码放在一起。这种类型的测试,开发人员是最适合、也是最有资格去做的人。
对于功能代码而言,思维模式是建设,重点在考虑用户、使用场景和数据流程上;对于测试代码而言,思维模式是破坏,借用代码扰乱用户以及数据。在理想的开发过程里,我们可以把开发人员分为功能开发人员与测试开发人员。
我们还需要一个用户开发人员,他们主要关心的问题是面向用户的任务,包括用例、用户故事、用户场景、探索式测试等等。
上面的这种乌托邦式的理想开发过程,三种角色分工合作,真是完美啊 O(∩_∩)O~
可惜,这种公司在现阶段好像还不存在过,Google 也只是接近这种模式。当前的软件业的发布周期需要以年为单位的客户端模式向每周、每天、甚至每小时都会发布的云端模式转变。Google 的客户端也按照 “云端模式” 来发布,他们的客户端都有一个“自动更新”功能。
Google 的软件测试开发工程师(SET)的职责如下:
- 在单元测试方面给予开发人员支持。
- 为开发人员提供测试框架,这样会方便开发人员编写一些中小型测试,以利于后期进行更多的与质量相关的测试。
1 开发和测试流程
公开的代码库(搜索非常便利)、和谐的工程工具、公司范围内的资源共享,这些共享服务依赖于 Google 的基础设施产品,它们会加速项目完成,并且减少了项目失败的风险。
开发人员在维护这些代码时,需要遵守这些规则:
- 所有的开发人员必须复用已经存在的公共库,除非在需求方面有很好的说服理由。
- 对于公共的共享代码,必须能够很容易被找到,并且具有良好的可读性,这些代码必须存储在代码库的共享区域,以便查找。
- 公共代码必须尽可能地被复用而且相对独立。因为与功能的复杂性和设计的巧妙性相比,可复用性带来的价值更大。
- 所有依赖必须明确指出。
- 如果一个开发人员对共享代码库中的某段代码有更好的方案,他可以去重构已有的代码,并协助依赖在这个公共代码库之上的应用项目迁移到新的代码库上。Google 设立了一个“同事奖金”,任何人如果受到其他开发人员的正面影响,就可以送出奖金作为感谢。除此之外,经理们还有权使用其他奖励手段。这么做的目的就是为了这种正向团队合作形成一种良性的循环,并持续发展下去。
- Google 非常重视代码审核,特别是公共通用模块的代码必须经过审核。开发人员必须通过相关语言(C++、Java、Python、JavaScript)的可读性审核,当开发人员按照约定的代码风格写出干净、简洁的代码之后,委员会会授予这名开发人员一个“良好可读性”的证书。
- 在共享代码库中的代码,对测试有更高的要求。
最小化对平台的依赖。所有开发人员的操作系统都尽可能地与 Google 生产环境的操作系统保持一致。对 Linux 发行版本也进行了管理。这样如果一个 bug 在测试机器上出现时,那么在开发机器和生产机器上应该也能复现。
所有对平台依赖的代码,都强制要求使用公共的底层库。Google 使用的每种编程语言,都要求使用统一的编译器,这个编译器针对不同的 Linux 发行版本都会进行持续的测试。限制运行环境可以避免许多与环境相关的那些难以调试的问题。保持简单,也就相对安全。
使用统一的运行平台和相同的代码库,进行持续集成测试、打包。
整体构建流程如下:
- 针对某个服务,保证所有相关代码编译通过。
- 设置这个服务的构建目标(Google 中是公共库、二进制文件或者测试套件,我们假设是公共库)
- 编写一套单元测试用例,所有外部重要的依赖通过 mock 模拟实现。
- 为单元测试创建一个测试目标。
- 构建并运行测试目标,有问题就修改代码,直到所有的测试都运行成功。
- 运行静态代码分析工具,确保遵守统一的代码风格,且通过一系列常见问题的静态扫描检测。
- 提交代码,申请代码审核,根据反馈再做修改,然后运行所有的单元测试并保证顺利通过。
这里面包含两个目标:
- 库构建目标:需要新发布的公共库。
- 测试构建目标:验证新发布的公共库是否满足需求。
一个 Google 产品由三部分组成:
- 经过良好测试的独立库。
- 可读性和可复用性好的公共服务库。
- 覆盖所有重要构建目标的单元测试套件。
为了保证单独的服务可以并行地开发,服务之间的接口需要在项目的早期确定下来,这样开发人员就可以依赖于协商好的接口上。为了保证服务级别之间的早期测试,这些接口一般只做了虚假实现。
在构建目标增长到一定规模时,针对功能集成的小型测试会成为回归测试的一部分。
在以上的活动中,SET 始终是核心参与者。SET 还会同时编写许多的 mock 工具。
2 SET 角色
SET 也是软件工程师,是一个 100% 的编码角色,他会作为测试人员尽可能早地参与到设计和代码开发流程中去。
SET 是和功能开发人员坐在一起的,这样更容易融入进去。
在面试 SET 的时候,在代码要求上与软件开发工程师(SWE)是一样的,同时还要求 SET 明白如何去测试 SWE 编写的代码。也就是 SET 的要求更高,需要同时了解代码以及测试的问题。
3 项目的早期阶段
许多创新产品(比如 Gmail 和 Chrome OS)都来源于团队 20% 的业余时间。只有在软件产品变得重要的时候,质量才显得重要。
如果一个产品在概念上还没有完全成型就去关心质量,这是优先级混乱的表现。因此在项目早期就强调测试,是一件非常愚蠢的事情。
Chrome OS 刚开始只有几个开发人员做了原型,且多数都是脚本和虚假实现,他们拿着只是原型的浏览器应用模型做演示,并通过了正式的立项批准。一旦得到正式批准立项,项目的开发总监就会找测试团队,寻求测试资源。
4 团队结构
SWE 一般仅在自己的模块领域内提供最优的解决方案,但从整个产品的角度来看,视野略窄,而一个 SET 不仅要具有更广的产品视野,而且在产品的整个生命周期中对产品和功能特性都要做充分的理解。
早期的计划做多少和怎样做比较合适,由创建项目的负责人来做最终的决定。
Google 的技术负责人一般由工程师担任,负责设定技术方向、开展合作、充当与其他团队沟通的项目接口人。
5 设计文档
在初期,团队成员一起协同完成设计文档的不同部分,这些文档需要技术负责人的审核。SET 在团队中的优势就是他们拥有产品方面最广阔的视野。
为什么要让 SET 参与审核设计文档:
- SET 熟悉系统设计(通过阅读所有设计文档)。
- SET 早期提出的建议会反馈在文档和代码里。
- 作为第一个审阅所有设计文档的人,SET 对整个项目的了解程度超过了技术负责人。
- SET 在项目初期就与开饭人员建立了良好的工作关系。
审核设计文档时,需要带着一定的目的性:
- 完整性:找出一些特殊背景知识的地方,鼓励作者在这方面添加更多的细节,或者增加一些外部文档的链接,作为补充。
- 正确性:是否有语法、错字、标点符号等方面的错误。
- 一致性:确保配图和文字描述保持一致,确保文档没有出现与其他文档在观点上截然相反。
- 设计:设计经过深思熟虑。
- 接口和协议:清晰的定义,完整地描述。
- 测试:保证系统的可测试性,而且易于测试。
6 接口和协议
Google 采用的是 protocol buffer 语言,它与编程语言和平台无关,对结构化数据具有可扩展性,但相比 XML 更小、更快、也更简单。开发人员使用 protocol buffer 的描述语言定义数据结构,然后自动生成源代码。
7 自动化计划
SET 要尽早提供一个可供实施的自动化测试计划。计划必须合情合理而且有影响力。因为投入的越多,维护的成本也会越大,所以需要保证计划规模小而且目的性要强。
SET 会先把容易出错的接口进行隔离,并针对它们创建 mock,这样就可以控制它们之间的交互,从而确保良好的测试覆盖率。
SET 会构建一个轻量级的自动化框架,并使用报表和仪表盘来展示收集到的测试结果以及测试进度,整个过程公开透明,这样获得的代码的质量会大大提高。
8 可测试性
SET 要保证系统的可测试性。他是一个质量顾问的角色,为开发人员提供程序结构和代码风格方面的建议,这样开发人员才能更好地做单元测试。SET 还提供测试框架方面的建议,使开发人员能够在测试框架的基础上写测试。
开发人员必须有能力进行代码审查,代码审查有来自工具和公司文化方面的支持。只有被证明是值的信赖的开发者,才能往代码库中提交代码。
Google 把代码审查作为开发流程的中心,因此相对于编写代码而言,代码审查更值的炫耀。
代码已一个“变更列表”(change list,下文会简称为 CL)的单元被编写和封装起来。CL 会被提交审查,Google 使用代码审查工具 Mondrian 发送给具有审核资格的 SWE 或 SET进行审查。
如果 CL 很大,那么审查者会要求把数量较大的 CL 分解为数量较小的几个 CL。有经验和值的信赖的开发人员会得到“可读性”的资格,大家同心协力确保整个代码库看起来像是由一个人编写的一样。
9 测试大小的定义
9.1 小型测试
小型测试就是单元测试。它一般集中精力在函数级别的独立操作与调用上,这样的限定可以提供更加全面的底层代码的覆盖率:
范围的隔离而且没有外部依赖,所以小型测试可以在很短的时间内运行结束。这样它们就可以执行的比较频繁,也可以很快发现问题。
9.2 中型测试
中型测试的主要目标是验证指定模块之间的交互,也就是集成测试。鼓励使用模拟技术(mock)来解决外部服务的依赖问题。有的情况下 mock 不能用,那就用轻量级的虚假实现(fake),比如使用常驻内存的数据库。
9.3 大型测试
大型测试就是系统测试,或者端到端测试。它会依赖外部资源,比如数据库、文件系统、网络服务等等。
10 测试平台
Google 的测试平台的需要满足的功能是:
- 开发人员编译和运行小型测试,希望立即能够知道运行结果。
- 开发人员系统运行一个项目中的所有小型测试,并能够快速知道运行结果。
- 只有在代码变更后,才希望去编译运行所有相关的测试,并能够立即知道运行结果。
- 看到一个项目的测试覆盖率并查看结果。
- 对项目的每次代码变更,都能够运行这个项目的小型测试,并将运行结果发送给团队成员以辅助进行代码审查。
- 在代码变更提高到版本控制系统后,自动运行项目的所有测试。
- 每周都能得到代码覆盖率,并实时跟踪覆盖率的变化。
当每一个测试都被标注为相应的规模后,调度器可以优化任务队列,达到合理利用的目的。
针对不同测试规模的测试执行时间的目标和限制:
要求 | 小型测试 | 中型测试 | 大型测试 | 超大型测试 |
---|---|---|---|---|
时间目标(每个函数) | 10ms 内 | 1s 内 | 尽可能快 | 尽可能快 |
强制时间限制 | 1 分钟后 | 5 分钟后 | 15 分钟后 | 1小时后 |
不同测试规模的资源使用情况:
资源 | 大型测试 | 中型测试 | 小型测试 |
---|---|---|---|
网络服务 | 是 | 仅本地 | 模拟 |
数据库 | 是 | 是 | 模拟 |
访问文件系统 | 是 | 是 | 模拟 |
访问用户界面 | 是 | 不鼓励 | 模拟 |
系统调用 | 是 | 不鼓励 | 否 |
多线程 | 是 | 是 | 不鼓励 |
睡眠状态 | 是 | 是 | 否 |
系统属性 | 是 | 是 | 否 |
Google 的测试系统会监测某个测试任务是否超时,或者消耗的资源超过了这个测试规模所应使用的资源时,会取消它并报告错误。
11 测试规模的益处
11.1 小型测试的优缺点
优点:
- 因为可以很快运行完毕,所以在有代码变更时就可以立即运行,这样可以较早地发现缺陷并及时反馈。
- 可以很容易做边界场景与错误条件的测试。
- 可以很容易地隔离错误。
缺点:
- 代码应清晰干净、规模较小且重点集中。
- 为了便于模拟,系统之间的接口要有良好的定义。
- 有时候对依赖资源的模拟是有难度的。
11.2 中型测试的优缺点
优点:
- 是小型测试到大型测试的过渡。
- 因为运行速度较快,所以也可以频繁地运行它们。
缺点:
- 依赖外部资源,所以本身就具有不确定性。
- 运行速度没有小型测试来得快。
11.3 大型测试的优缺点
优点:
- 是最根本,也是最重要的,因为它们反应了系统是如何工作的。
缺点:
- 依赖外部资源,所以本身就具有不确定性。
- 因为测试范畴很宽,所以如果测试运行失败,要精确定位到失败的根源比较困难。
- 准备测试数据很耗时。
- 大型测试不能像小型测试那样可以走特定的代码路径,所以只能进行功能的常规测试。
小型测试带来优秀的代码质量、良好的异常处理以及优雅的错误报告。中大型测试带来整体产品质量和数据验证。
代码覆盖率的结果会存储在云端,任何开发人员都可以通过内网的网络,使用浏览器来查看这些报告。
总体上的经验法则是:70% 是小型测试,20% 是中型测试,10% 是大型测试。如果项目是面向用户的,拥有较高的集成度,或者用户接口比较复杂,就应该使用更多的中大型测试;如果是基础平台或者面向数据的项目,最好有大量的小型测试。
12 测试运行的要求
- 每个测试与其他测试都是独立的,它们能够以任意顺序运行。
- 不做持久化方面的工作。当测试用例测试后,要保证测试环境的状态与测试用例开始执行之前的状态是一致的。
add 小型、中型、大型测试的优缺点 2017/4/6