《ThoughtWorks 实践集锦》(第二册)

原载于 InfoQ 中文站”ThoughtWorks 实践集锦”专栏,约 2009-2010 年。紧扣”实效”二字,关注如何使用实践、创造实践来解决实际问题。 主编/作序:郭晓(ThoughtWorks 中国公司总经理)

本书是 ThoughtWorks 中国咨询师团队的实战经验文集,共收录 11 篇文章,覆盖测试协作、数据迁移、富客户端开发、版本控制、持续集成、环境管理、Tech Lead 角色、自动化测试分层、TDD 实践等主题。


全书概览

章节标题作者主题领域
第1章我和敏捷团队的五个约定覃其慧测试与团队协作
第2章如何在敏捷开发中做好数据迁移章昱恒数据库/数据迁移
第3章RichClient/RIA 原则与实践(上)陈金洲富客户端架构
第4章RichClient/RIA 原则与实践(下)陈金洲富客户端架构
第5章为什么我们要放弃 Subversion胡凯版本控制/DVCS
第6章”持续集成”也需要重构乔梁持续集成演进
第7章Mock 不是测试的银弹胡凯测试策略
第8章环境无关的环境李光磊开发环境管理
第9章Tech Lead 的三重人格熊节技术领导力
第10章自动化测试的分层结构李贝测试架构
番外篇TDD 之实践主义李光磊TDD 实践技巧

第1章:我和敏捷团队的五个约定

作者:覃其慧 —— 以测试人员视角提出与团队协作的 5 个约定,核心是”预防胜于治疗”和价值驱动的测试。

约定1:和 BA 是同一个角色的两种面孔

  • 测试人员应尽早参加客户需求会议,在需求定义阶段就准备好测试用例
  • 在开发写代码之前告诉他们”我要测什么”,让开发减少因乐观而漏掉的破坏性场景
  • 核心:把测试工作前移,从”找 bug”变成”预防 bug”

约定2:开发是自动化测试专家,但请听听测试的意见

  • 开发的单元/集成测试离代码近、离最终用户远,很多不是在测功能
  • 测试人员可以指出:什么功能测试最该自动化、什么地方缺陷可能最频繁
  • 测试视角补充开发视角,让自动化测试投入产出比更高

约定3:不要要求测试所有路径

  • 软件测试是永无止境的任务,连计算器都有无穷多路径
  • 测试人员了解软件价值,可以基于价值做判断:什么是至关重要的、什么是不太可能出现的
  • 基于价值分配测试精力,而不是追求穷尽

约定4:交付风险有疑问,请来问测试

  • BA 和 Dev 关注”软件在什么情况下正常工作”,QA 还大量关注”什么情况下不正常”
  • 探索性测试会发现未定义的、不曾预期的行为,这些构成交付风险
  • 测试人员能告诉你:问题分布在哪、是否牵连其他部分、能不能绕过去

约定5:敏捷实践对测试人员也有用

  • 结对不是 dev 的专利:测试之间结对看用例是否全面,测试与开发结对写自动化测试
  • 发现架构决定使测试变难时,大声说出来 —— design for testability
  • 需求无法验证时,提醒 BA 遵循 INVEST 原则(独立、可协商、价值、可估算、短小、可测)

第2章:如何在敏捷开发中做好数据迁移

作者:章昱恒 —— 以 CRM 系统新旧迁移项目为背景,讲解如何高质量做数据迁移。

数据迁移常被低估

  • 常被视为”写几条 SQL 装数据”的简单工作
  • 实际涉及:业务逻辑模糊、脏数据、遗留系统技术债和管理债
  • 在电信/金融等数据核心行业,数据质量就是软件质量的权重部分

方法论:DBA 也要管理好自己的任务

  1. 制定目标、管理任务:DBA 有自己的故事墙,避免数据迁移任务与其他数据库任务冲突
  2. 从商业价值决策迁移需求:积极参加 IPM,通过沟通发现潜在数据需求,排优先级
  3. 思考实施策略再动手:先识别风险(数据质量、对原系统了解程度、业务映射),再定策略

数据迁移的两类场景

维度一次性数据迁移数据同步迁移
数据量大小
使用频率一次性高频(分钟级周期)
转换逻辑复杂,大量定制映射较复杂,定制映射
事务要求需要保证一致性需要事务控制
测试侧重数据质量测试逻辑映射测试
工具选择直接 SQL 脚本,提高效率可考虑第三方工具增强事务
中间结果保留可不保留

三个核心实践

(1) 细化任务

  • 把复杂迁移需求分解为若干模块,画出整体结构图
  • 小粒度任务能暴露更多问题,让团队从”觉得很难”变成”每个小问题都容易解决”

(2) 测试驱动

  • 为迁移脚本写测试,形成安全网,异常数据出现时能立即发现
  • 三类测试:
    • 应产生的符合期望的数据 — 验证转换逻辑正确
    • 不应产生的异常数据 — 发现脏数据、误操作、系统 bug
    • 数据量是否符合期望 — 确保数据没有丢失
  • 原则:一段 SQL 只测一处期望,减少相互依赖,准确定位错误

(3) 持续集成

  • 三级测试沙盒:
    • 开发沙盒:少量核心数据,测脚本质量
    • 系统集成沙盒:小型数据库,测逻辑转换
    • 生产环境级沙盒:真实数据备份,持续跑测试避免脏数据和丢失
  • 用自动化工具(如 NAnt)组织:初始化 → 编译部署 → 重置测试数据 → 执行迁移 → 执行测试

关键洞察:数据迁移不是一次性工作,而是与整个敏捷开发过程同步演进的持续活动。测试驱动和持续集成是保障数据质量的两道安全网。


第3-4章:RichClient/RIA 原则与实践

作者:陈金洲 —— Buffalo Ajax Framework 作者,多个富客户端项目经验。

背景

  • Web 开发已有成熟积累(JavaEE/.NET/RoR 各有最佳实践),但 RichClient 企业开发领域经验匮乏
  • 大多数书偏小规模特性介绍,对大规模企业应用架构决策帮助很小
  • 作者使用过 Java Swing、Flex/Air、.NET WinForm/WPF,总结出跨平台通用的原则

核心原则

1. 一切皆异步

  • 所有耗时操作都应该异步进行,这是第一条也是最重要的原则
  • 违背会导致 UI 线程阻塞、界面冻结,用户会认为”程序把我机器弄死了”
  • 用户期待级别:能用 → 可用 → 好用 → 好看,多数程序员停留在”能用”
  • 实现方式:耗时操作放后台线程,UI 更新放回 UI 线程

2. 线程管理

  • 与”一切皆异步”直接关联
  • (下篇详细展开,需结合线程模型、线程池、UI 调度等)

3. 事件管理

  • 富客户端交互复杂,事件多且易产生循环依赖
  • 需要统一的事件管理机制,避免内存泄漏和逻辑混乱

4. 缓存与本地存储

  • 减少网络请求,提升响应速度
  • 需要考虑缓存失效策略、数据一致性

5. 数据交互模式

  • 与缓存/本地存储强关联
  • 需要设计好客户端-服务器的数据同步策略

原则之间不是孤立的:遵循”一切皆异步”就需要”线程管理”和”事件管理”;引入”缓存与本地存储”就要考虑”数据交互模式”。


第5章:为什么我们要放弃 Subversion

作者:胡凯 —— 从 SVN 迁移到 Mercurial 的实战经验。

Subversion 的天生缺陷

  1. 无法适应分布式团队:服务器只能架在一地,跨国团队操作缓慢
  2. 单点故障风险高:所有元数据只保存在服务器上,服务器故障导致数据回滚(曾回滚到 9 天前)
  3. 伸缩性差:无法根据团队规模和结构灵活调整

转向 DVCS(以 Mercurial 为例)的收益

快速可靠

  • 本地仓库包含全部元数据,提交、追溯历史、更新等操作无需联网
  • 中央仓库出问题时,任何一个工作目录都可以成为备用仓库,避免”所有鸡蛋放一个篮子”

便于协同工作

  • 灵活的分支合并:可以从任意同事的机器克隆代码,不依赖中央服务器
  • 跨平台协作:Linux 上开发,Windows 上验证,通过本地克隆快速同步

对小步前进友好

  • 本地提交自由度高,允许频繁提交而不必顾忌每次都不破坏现有功能
  • 代码稳定后再与中央仓库同步
  • 降低了”小步前进”的技术门槛,团队可以先实践再追求质量

学习曲线低

  • 可以采用与 CVCS 非常相似的架构(中央仓库 + 工作副本)
  • 基本命令与 CVS/SVN 类似
  • 可用 HgSubversion 插件渐进式过渡(Mercurial 当客户端,SVN 服务器保留)

迁移建议

  1. 先识别团队痛点,不要追赶技术潮流
  2. 实际使用、慢慢接受,光靠理论论证学不会 DVCS
  3. 持续学习 DVCS 背后的设计思想、问题域抽象和插件生态

第6章:“持续集成”也需要重构

作者:乔梁 —— Cruise 产品项目经理,资深敏捷过程教练。

核心观点

  • 持续集成不是部署完就一劳永逸的,它和产品代码一样需要持续改进和重构
  • 否则会从”持续集成”变成”持续闹心”
  • 应该对持续集成实践本身应用 Retrospective 和重构

演进阶段

阶段一:基本持续集成

  • 典型流水线:CheckStyle → Compile → UnitTest → FunctionTest → Report
  • 单台机器运行,几分钟完成
  • 用 CruiseControl 等工具建立基础 CI 服务器

阶段二:(演进中,需结合全文)

  • 随着代码量增长、测试增多,构建时间变长
  • 需要引入:
    • 流水线分解:把构建拆分为多个阶段,快速反馈在前,慢测试在后
    • 分布式构建:多台 agent 并行执行
    • 构建管道:build pipeline,不同阶段有不同的质量门禁
    • 发布流水线:从提交到发布的全流程自动化

注:全文详细描述了 Cruise 团队自身 CI 的演进路径,包括从单阶段到多阶段、从集中到分布的重构过程。核心方法论是:持续集成本身就是产品,需要迭代演进。


第7章:Mock 不是测试的银弹

作者:胡凯 —— 从 Cruise 产品的真实教训出发,讨论 Mock 的局限性。

Mock 的行为依赖风险

  • Mock 测试看起来完美,但危机暗藏:被模拟对象的行为与真实对象必须完全一致
  • 真实案例:Perforce 命令行的 stdout 时间格式会因用户环境设置而变化,但 Mock 测试用的是固定样本
  • 结果:单元测试覆盖率 >80%,但发布前全面测试仍频繁发现严重缺陷
  • 非法测试:看起来像测试、运行起来像测试,但几乎没有价值、几乎不会失败

为什么 Mock 不是银弹

  • 开发者对 API 了解不够 → 错误假设
  • 被模拟对象行为变化(重构、新功能)→ 假设失效
  • 层层 Mock 意味着层层假设,端到端功能测试又太少 → 假安全

引用《UNIX 编程艺术》:“先求运行,再求正确,最后求快。“正确性是速度和隔离性的基础,离开了正确性都是无根之木。

编写健壮真实测试的秘密

1. 设计合理的等待策略

  • 不要用 Thread.sleep(万恶之源),它会在不同机器上引起随机失败
  • 用轮询方式检查外部系统状态(文件存在、端口打开等),状态满足才运行断言
  • 设计合理的 timeout,避免无限等待

2. 正确创建和销毁资源

  • 原则:谁创建,谁销毁
  • 问题:JUnit 的 @Before/@After 模式导致 teardown 膨胀(要处理所有测试用例资源的并集)
  • 解决方案:使用 Precondition 模式(作者开源 junit-ext 项目)
    @Preconditions({ResourceIsCreated.class, ServiceIsStarted.class})
    @Test
    public void test1() { ... }
  • 每个资源类自己管理 setup/teardown,减少测试间依赖

3. 设计合理的过滤策略

  • 有些测试只能在特定环境运行(特定 OS、特定浏览器)
  • 用标注机制有选择地运行:@RunIf(PlatformIsWindows.class)
  • 比 JUnit 的 assumeThat 更灵活

4. 用计算资源而非人力资源加快测试

  • 不推荐多线程并发跑测试(会让测试编写和问题定位变复杂)
  • 推荐多机器并行:每台机器跑一部分测试,时间近似 N 分之一
  • 利用闲置机器或虚拟机,硬件一次性投入比持续手工优化成本低

第8章:环境无关的环境

作者:李光磊 —— 创建可复制、可移植的开发/测试/构建环境。

问题症状

  1. 产品只能在你的机器上编译通过
  2. 你机器上正常,测试环境总出错
  3. 新人加入要花一天搭环境
  4. 迁移环境要几天时间

核心原则与实践

1. 用相对路径代替绝对路径

  • Windows: %~dp0(脚本所在路径,比 %cd% 更常用)
  • Unix Bash: $(pwd) 或 $PWD
  • Makefile: $(shell pwd)
  • Ant: basedir 属性
  • 显式定义根路径变量,提供间接层以便覆盖

2. 使用配置管理系统(版本控制)

  • 配置管理不只是放源代码的,环境配置文件也应该入版本控制
  • 解决:目录结构不固定、丢三落四、工具版本不一致
  • 大型系统软件(JDK、编译器)不用入版本控制,但脚本里要检查版本
  • 二进制依赖用 Maven/Ivy 等依赖管理工具
  • 必须放特定位置的文件(如 /etc/ 下):入版本控制 + 符号链接 + 自动脚本

3. 环境变量

  • 全局环境变量用作缺省值,脚本中覆盖它

4. 缺省值 + 用户自定义属性(核心机制)

  • 问题:所有配置都入版本控制,但每台机器又需要不同的本地修改 → 产生未提交的本地修改 → 升级时冲突
  • 解法:用户自定义属性放单独文件,不提交到版本控制
    • Windows: call user_env.bat
    • Bash: source ./user_env.sh
    • Ant: <property file="user.properties"/>
  • 注意 Ant 属性是”先入为主”(只读),CruiseControl 则是”后发制人”

更进一步

  • 提供生成器脚本,自动探测环境生成配置(类似 Rails 生成应用框架)
  • 使用虚拟机统一项目组开发环境(Neal Ford《卓有成效的程序员》建议)

第9章:Tech Lead 的三重人格

作者:熊节 —— Tech Lead = 技术决策者 + 流程监督人 + 干扰过滤器。

第一重人格:技术决策者

三个关注点:

(1) 参与架构设计

  • 选什么平台和语言?
  • 和什么系统集成?用哪些第三方工具?
    • 给自己画一张集成架构图:虚线圈整个系统,实线圈自主开发部分,其余是集成目标
  • 如何部署?如何迁移数据?
    • 部署频度、各环境由谁部署、测试数据集从哪来
  • 团队技能与项目需求是否匹配?

(2) 攻克技术难题

  • 集成点是否妥善处理?集成测试环境到位了吗?
  • 技术栈某部分出问题怎么办?
    • 谁熟悉这个组件?开源社区在哪?闭源的话技术支持在哪?

(3) 确定设计方案

  • 代码中是否出现明显的 bad smell?(大类、长方法、重复代码、深层条件嵌套、逻辑放错层)
  • 局部设计是否会严重损害系统?(性能、安全、可测性、概念一致性)
  • 团队是否在讨论设计和重构?
    • 不写代码的人无权评价代码
    • 好实践:每天早上 15-20 分钟集体浏览前一天的代码(类似每日 code review)

第二重人格:流程监督人

三个关注点:开发环境、持续集成、测试。

(文中详述,核心是确保开发流程顺畅、快速反馈、质量有保障。)

第三重人格:干扰过滤器

  • 保护团队不受外界干扰,让开发者专注开发
  • 对外沟通、协调资源、应对变更请求,而不是让团队直接面对

核心洞察:Tech Lead 不只是技术最好的人,更是要承担技术决策、流程保障和团队保护三重职责的角色。


第10章:自动化测试的分层结构

作者:李贝 —— 借鉴软件架构的分层思想,解决测试代码的可维护性问题。

问题:测试代码的”大泥球”

  • 测试代码中混合了:URL 拼接、HTML/XML 解析、UI 控件访问、测试逻辑
  • 导致三个问题:
    1. 测试逻辑难以理解和修改 —— 夹杂大量支撑代码,辨别不出真正的测试逻辑
    2. 测试很脆弱 —— UI 一变(比如元素挪位置、ID 改了),所有相关测试都挂
    3. 维护开销大 —— 重复代码遍布各个用例,改一处要改 N 个地方

解决方案:三层架构

┌─────────────────────────────────┐
│     测试用例层 (Test Case)       │  ← 只表达测试逻辑,用业务领域语言
├─────────────────────────────────┤
│     领域层 (Domain Layer)        │  ← 封装技术细节,提供领域接口
├─────────────────────────────────┤
│     待测系统层 (SUT)             │  ← 真实被测系统
└─────────────────────────────────┘

测试用例层

  • 只包含测试逻辑,清晰简洁
  • 用自然语言/业务语言表达,非技术人员也能读懂
  • 不同场景的区别只在于测试数据
  • 框架示例:Cucumber(Ruby)、NBehave(C#)等 BDD 框架

领域层

  • 封装所有技术细节:URL 拼接、HTTP 请求、XML/HTML 解析、浏览器控制等
  • 用业务领域术语建模(类名和方法名跟真实系统一致)
  • 测试用例通过领域对象交互,不直接接触技术细节

代码示例

测试用例层(接近自然语言):

[Story]
public class SearchCustomerStory {
    [Scenario]
    public void SearchWithExactMatch() {
        story.Given(AN_ACCOUNT_WITH_PHONE_NUMBER, "01068930432")
             .When(SEARCH_WITH, "01068930432")
             .Then(ACCOUNT_INFO_SHOULD_BE_RETURNED, "13120205504");
    }
}

领域层(封装技术细节):

public class CustomerService {
    public Subscriber SearchWithTelephoneNumber(string telephoneNumber) {
        string url = $"{endpoint}/subscribers?telephoneNumber={telephoneNumber}";
        return GetResponse(url); // 内部做 HTTP 请求 + 解析
    }
}

如何解决三大问题

  1. 难理解难修改 → 测试用例层独立存在,用业务语言简洁表达,读测试考验的是语言能力而非编码水平
  2. 测试脆弱 → 系统变化只影响领域层一处,用例层不受影响
  3. 维护开销大 → 重复代码收敛在领域层,修改只需改一处

常见疑问

  • 是不是太复杂? → 看系统规模。小系统可能没必要,企业级应用则收益很大
  • 前期搭建是不是浪费? → 只是换种组织代码的方式,不需要一次做完,可以逐步演进
  • QA 不会写 OO 代码怎么办? → 测试自动化是整个团队的事。开发写领域层,QA 写测试用例层,各展所长

番外篇:TDD 之实践主义

作者:李光磊 —— 三个实用的 TDD 技巧,不争论哲学,只解决实际问题。

1. 为沟通选择语言(用中文写测试)

  • 问题:领域术语难翻译,翻译后辞不达意,过段时间自己也看不懂
  • 解法:直接用中文写测试方法名
    public void test_应该算未通过_if_职务高于二副二管轮_而_学历只是中专_并且_毕业时间晚于2002年2月1日()
  • 权衡:
    • 放弃了”应该用英文”的原则
    • 但获得了:代码交流效率大幅提升、测试用例名与验收条件几乎一模一样
  • 判断依据:团队不善长领域英语、翻译有信息损失、可预见的将来没有老外加入
  • 工程是折衷的艺术:学术理论可以极端,工程一定是各种应力的人为平衡

2. 用大量测试来驱动(先写所有用例名)

  • 问题:TDD 想一点写一点,容易漏掉验收条件
  • 解法:先把需求文档里所有验收条件变成空的测试函数(函数体只有 // TODO)
    • 两分钟就能把整个用户故事的测试用例全部描述出来
    • 空函数体的测试都通过,不妨碍提交和 CI
    • 每次看到空函数体就提醒你还有活没干完
  • 好处:
    • 需求就在代码里,不用跑去翻文档
    • 仍然保持小步前进:一个测试 → 实现 → 下一个测试
    • 需求变化时,改函数名成本极低,不会浪费
  • 原则:用测试用例的名字来描述需求

3. 一个环境,多个断言

  • 问题:测试变慢,瓶颈在于搭建环境(尤其是 Selenium 测试要启动浏览器)
  • 冲突:Kent Beck 说”每个测试用例一个断言”,但一个环境只断言一次太浪费
  • 解法:一个测试用例里调用多个有描述性名称的断言函数
    @Test
    public void test_should_show_step_details_info_in_todo_item_page() {
        TodoItemPage page = navigator.gotoTodoItemPage();
        should_show_step_name_as_page_title(step, page);
        should_show_start_button_if_waiting(page);
        should_show_transition_buttons(step, page);
        should_show_comment_box_after_click_start(page);
    }
  • 既保留了什么?又获得了什么?
    • 保留:每个断言都有清晰精确的描述(通过函数名)
    • 获得:多个断言共享一次环境搭建,测试速度大幅提升
  • 原则背后的理念才是重要的,原则本身是手段不是目的

核心主题总结

全书贯穿几个统一的思想线索:

  1. 实效优先,不教条

    • TDD 可以用中文写测试、可以先列所有用例名、可以一个用例多个断言
    • 版本控制不追新,是因为 SVN 真的解决不了分布式团队问题
    • Mock 不是银弹,该用真实环境就用真实环境
  2. 分层与抽象的普适性

    • 自动化测试分层(测试用例层 / 领域层 / 待测系统层)
    • 持续集成演进(从单阶段到多阶段流水线)
    • 富客户端原则之间的关联网络
  3. 前移与预防

    • 测试前移到需求阶段,预防胜于治疗
    • 数据迁移用测试驱动,问题越早发现成本越低
    • 环境问题标准化,避免事后救火
  4. 工具是手段,团队效能是目的

    • 选 DVCS 不是追潮流,是因为团队结构变了
    • 持续集成要持续重构,因为项目在演进
    • 技术选型始终回到”解决了什么实际问题”

关联笔记