《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 也要管理好自己的任务
- 制定目标、管理任务:DBA 有自己的故事墙,避免数据迁移任务与其他数据库任务冲突
- 从商业价值决策迁移需求:积极参加 IPM,通过沟通发现潜在数据需求,排优先级
- 思考实施策略再动手:先识别风险(数据质量、对原系统了解程度、业务映射),再定策略
数据迁移的两类场景
| 维度 | 一次性数据迁移 | 数据同步迁移 |
|---|---|---|
| 数据量 | 大 | 小 |
| 使用频率 | 一次性 | 高频(分钟级周期) |
| 转换逻辑 | 复杂,大量定制映射 | 较复杂,定制映射 |
| 事务要求 | 需要保证一致性 | 需要事务控制 |
| 测试侧重 | 数据质量测试 | 逻辑映射测试 |
| 工具选择 | 直接 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 的天生缺陷
- 无法适应分布式团队:服务器只能架在一地,跨国团队操作缓慢
- 单点故障风险高:所有元数据只保存在服务器上,服务器故障导致数据回滚(曾回滚到 9 天前)
- 伸缩性差:无法根据团队规模和结构灵活调整
转向 DVCS(以 Mercurial 为例)的收益
快速可靠
- 本地仓库包含全部元数据,提交、追溯历史、更新等操作无需联网
- 中央仓库出问题时,任何一个工作目录都可以成为备用仓库,避免”所有鸡蛋放一个篮子”
便于协同工作
- 灵活的分支合并:可以从任意同事的机器克隆代码,不依赖中央服务器
- 跨平台协作:Linux 上开发,Windows 上验证,通过本地克隆快速同步
对小步前进友好
- 本地提交自由度高,允许频繁提交而不必顾忌每次都不破坏现有功能
- 代码稳定后再与中央仓库同步
- 降低了”小步前进”的技术门槛,团队可以先实践再追求质量
学习曲线低
- 可以采用与 CVCS 非常相似的架构(中央仓库 + 工作副本)
- 基本命令与 CVS/SVN 类似
- 可用 HgSubversion 插件渐进式过渡(Mercurial 当客户端,SVN 服务器保留)
迁移建议
- 先识别团队痛点,不要追赶技术潮流
- 实际使用、慢慢接受,光靠理论论证学不会 DVCS
- 持续学习 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. 用相对路径代替绝对路径
- 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"/>
- Windows:
- 注意 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 控件访问、测试逻辑
- 导致三个问题:
- 测试逻辑难以理解和修改 —— 夹杂大量支撑代码,辨别不出真正的测试逻辑
- 测试很脆弱 —— UI 一变(比如元素挪位置、ID 改了),所有相关测试都挂
- 维护开销大 —— 重复代码遍布各个用例,改一处要改 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 请求 + 解析
}
}如何解决三大问题
- 难理解难修改 → 测试用例层独立存在,用业务语言简洁表达,读测试考验的是语言能力而非编码水平
- 测试脆弱 → 系统变化只影响领域层一处,用例层不受影响
- 维护开销大 → 重复代码收敛在领域层,修改只需改一处
常见疑问
- 是不是太复杂? → 看系统规模。小系统可能没必要,企业级应用则收益很大
- 前期搭建是不是浪费? → 只是换种组织代码的方式,不需要一次做完,可以逐步演进
- 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); } - 既保留了什么?又获得了什么?
- 保留:每个断言都有清晰精确的描述(通过函数名)
- 获得:多个断言共享一次环境搭建,测试速度大幅提升
- 原则背后的理念才是重要的,原则本身是手段不是目的
核心主题总结
全书贯穿几个统一的思想线索:
-
实效优先,不教条
- TDD 可以用中文写测试、可以先列所有用例名、可以一个用例多个断言
- 版本控制不追新,是因为 SVN 真的解决不了分布式团队问题
- Mock 不是银弹,该用真实环境就用真实环境
-
分层与抽象的普适性
- 自动化测试分层(测试用例层 / 领域层 / 待测系统层)
- 持续集成演进(从单阶段到多阶段流水线)
- 富客户端原则之间的关联网络
-
前移与预防
- 测试前移到需求阶段,预防胜于治疗
- 数据迁移用测试驱动,问题越早发现成本越低
- 环境问题标准化,避免事后救火
-
工具是手段,团队效能是目的
- 选 DVCS 不是追潮流,是因为团队结构变了
- 持续集成要持续重构,因为项目在演进
- 技术选型始终回到”解决了什么实际问题”
关联笔记
- 《ThoughtWorks文集》-精选版 — 第一册,更多架构、重构、设计等主题
- 《重构》-改善既有代码的设计 — 代码重构方法论
- 《SICP》-计算机程序的构造和解释 — 计算机科学基础
- 《纳瓦尔宝典》-财富和幸福指南 — 个人成长与财富积累
- 《卓有成效的程序员》-Neal Ford — 环境管理、规范性原则的源头