第一部分:基础篇
第 1 章:软件交付的问题
核心痛点与反模式
- 手动部署软件: 过程冗长、易错、不可重复,且高度依赖个人经验。
- 开发完成后才在类生产环境中部署: 直到项目后期才发现环境、配置和架构问题,修复成本极高。
- 生产环境的手动配置管理: 导致环境不一致(“配置漂移”),发布失败难以追溯。
核心理念:部署流水线 (Deployment Pipeline)
- 部署流水线是将软件从版本控制库到用户手中的这个过程的自动化实现。
- 目标是让发布过程变得可重复、可靠、可预测,从而降低风险,缩短交付周期。
- 流水线的三个目标:可视化(让流程对所有人可见)、快速反馈(尽早发现问题)、持续部署(随时能按需部署任意版本)。
持续交付的八大原则
- 为软件发布构建可重复且可靠的流程。
- 自动化几乎所有事情。
- 把所有东西都纳入版本控制。
- 如果一个过程是痛苦的,就更频繁地去做它,并尽早地暴露痛苦。
- 内建质量,而不是在流程末端检查质量。
- “完成”意味着已发布给用户并创造价值。
- 交付过程是每个人的责任。
- 持续改进。
第 2 章:配置管理
第 2 章:配置管理
版本控制 (Version Control)
- 所有东西都纳入版本控制: 不仅是源代码,还包括测试脚本、数据库脚本、构建部署脚本、配置文件、环境定义、文档等一切工件。
- 目标是能够从版本库中精确重现任何环境和应用的任何版本。
- 频繁地向主干(Trunk/Mainline)提交代码。
依赖管理 (Dependency Management)
- 明确管理第三方库和内部组件的依赖关系,锁定版本以保证构建的可重现性。
- 不要在构建时从互联网上随意下载依赖,应建立组织内部的私有仓库(Artifact Repository)。
应用配置管理
- 将配置与代码分离。同一个二进制包应该能部署到任何环境,区别仅在于配置。
- 配置应该在部署时或运行时注入,而不是在构建时打包进去。
- 将所有环境(开发、测试、生产)的配置文件都纳入版本控制。
- 像对待代码一样对待配置:管理它、测试它。
环境管理
- 环境的创建和配置应该是完全自动化的脚本。
- 任何对环境的修改都应通过修改版本库中的脚本并执行自动化流程来完成,禁止手动登录服务器修改。
- 使用 Puppet, Chef, Ansible 等工具来自动化基础设施的配置。
第 3 章:持续集成
第 3 章:持续集成 (Continuous Integration, CI)
CI 的核心实践
- 每次代码提交都自动触发构建和运行自动化测试。
- CI 的目标是让软件时刻处于可工作状态,并在破坏发生的瞬间发现它。
- 如果构建或测试失败,整个团队的最高优先级任务就是立即修复它。
CI 的前提条件
- 使用版本控制系统,并且所有人都向单一主干提交。
- 拥有一个可以从命令行启动的自动化构建过程。
- 拥有一个快速且全面的自动化测试套件。
关键纪律
- 不在破碎的构建(Broken Build)基础上提交代码。
- 提交前在本地运行所有提交阶段测试(Commit Tests)。
- 不要在构建破碎时下班回家。如果无法快速修复,就回滚(Revert)你的提交。
- 不要通过注释掉失败的测试来“修复”构建。
第 4 章:测试策略实现
第 4 章:测试策略的实现
测试象限 (Testing Quadrants)
- Brian Marick 提出的模型,将测试分为四个象限,以区分测试的目标(支持开发 vs. 批评产品)和视角(面向业务 vs. 面向技术)。
- Q1 (面向技术,支持开发): 单元测试、组件测试。由开发者编写,保证代码质量。
- Q2 (面向业务,支持开发): 验收测试、功能测试、故事测试。由团队(包括业务人员、测试、开发)共同定义,保证软件满足业务需求。
- Q3 (面向业务,批评产品): 探索性测试、场景测试、可用性测试。主要由人工完成,探索软件的未知行为和用户体验。
- Q4 (面向技术,批评产品): 性能测试、负载测试、安全测试。验证非功能性需求。
自动化测试金字塔 (Test Automation Pyramid)
- 这是一个指导自动化测试投入比例的模型。
- 底层 (最宽): 单元测试。数量最多,运行速度最快,反馈最及时。
- 中层: 组件/服务/集成测试。数量适中,测试组件间的交互。
- 顶层 (最窄): 端到端/UI 测试。数量最少,运行最慢,最脆弱,但最贴近用户真实操作。
- 违背金字塔结构(如大量UI测试,少量单元测试)会导致测试套件维护成本高、运行慢、反馈周期长。
第二部分:部署流水线
第 5 章:部署流水线的剖析
流水线的阶段 (Stages)
- 提交阶段 (Commit Stage): 这是流水线的入口。每次代码提交后触发,执行快速的构建、代码分析和单元测试。目标是在几分钟内给出反馈。
- 自动化验收测试阶段 (Automated Acceptance Test Stage): 在提交阶段成功后自动触发。在一个类生产环境中部署应用,并运行端到端的自动化验收测试。
- 后续测试阶段 (Subsequent Test Stages): 可能包括容量测试、集成测试、UI兼容性测试等。这些阶段可以并行执行。
- 手动测试阶段 (Manual Test Stage): 在所有自动化测试通过后,测试人员或业务人员可以按需将指定版本部署到手动测试环境(如UAT环境)进行探索性测试、可用性测试等。
- 发布阶段 (Release Stage): 部署到生产环境。这通常是一个需要授权的手动触发操作,但其执行过程是完全自动化的。
核心实践
- 二进制包只构建一次: 在提交阶段生成二进制包(如JAR, DLL, Docker镜像),并在后续所有阶段重用这个包。这确保了你测试的东西就是你将要发布的东西。
- 对所有环境使用相同的部署机制: 无论是开发、测试还是生产环境,都应使用同一套自动化脚本进行部署,区别仅在于配置。
- 对部署进行冒烟测试: 部署完成后,立即运行一小组快速测试,验证应用已正确启动并能提供基本服务。
- 流水线中任何环节失败,就停止整个流水线: 立即修复问题是最高优先级。
第 6 章:构建和部署脚本化
第 6 章:构建和部署脚本化
构建工具
- 工具分为面向任务(如 Ant)和面向产物(如 Make)。现代工具(如 Gradle, Rake)通常是混合型。
- 推荐使用内部DSL(如 Rake, Gradle, Psake)的构建工具,因为它们使用通用编程语言,更灵活、更易于维护和调试。
- 像 Maven 这样的工具通过“约定优于配置”简化了标准项目的构建,但自定义时可能变得复杂。
脚本化原则
- 为流水线的每个阶段创建独立的脚本。
- 使用与目标环境相适应的技术进行部署(例如,使用操作系统自身的打包工具,如 RPM, DEB, MSI)。
- 确保部署过程是幂等的(Idempotent):无论执行多少次,结果都应相同。
- 脚本中始终使用相对路径,避免硬编码绝对路径。
- 从二进制包到版本控制应有可追溯性,例如在包的元数据中嵌入版本号和Commit ID。
第 7 章:提交阶段
第 7 章:提交阶段
目标与原则
- 提交阶段的核心目标是快速失败,为开发者提供最快的反馈。
- 理想的运行时间应在5分钟以内,绝不应超过10分钟。
- 此阶段的成功产物是可部署的二进制包和各种报告(测试报告、代码分析报告),它们将被存入工件库(Artifact Repository)。
提交阶段包含的任务
- 编译代码。
- 运行提交测试套件(主要是单元测试,可包含少量快速的组件/集成测试)。
- 进行静态代码分析(检查代码风格、复杂度、重复代码等)。
- 打包成可部署的二进制文件。
提交测试套件的设计原则
- 避开UI: UI测试太慢且脆弱,不适合提交阶段。
- 避开数据库: 依赖数据库的测试通常也较慢。使用测试替身(Test Double)如 Mocks 和 Stubs,或使用内存数据库。
- 避开异步: 在单元测试中避免处理复杂的异步逻辑,将其分解为同步部分进行测试。
- 使用依赖注入(Dependency Injection)来解耦组件,使其易于被测试替身替换。
第 8 章:自动化验收测试
第 8 章:自动化验收测试
为什么自动化验收测试至关重要
- 它是验证软件是否满足业务价值的唯一可靠方式。单元测试只能证明代码按开发者意图工作,但不能证明满足用户需求。
- 它构成了强大的回归测试安全网,使得大规模重构和架构演进成为可能。
- 将测试人员从重复性的回归测试中解放出来,让他们专注于高价值的探索性测试。
可维护的验收测试分层架构
- 第一层 (验收标准层): 使用自然语言描述业务规则(例如,使用 Gherkin 语法的 "Given-When-Then" 格式)。工具如 Cucumber, JBehave。
- 第二层 (测试实现层): 将自然语言步骤翻译成调用领域语言的代码。
- 第三层 (应用驱动层): 封装了与被测系统交互的所有细节。这是唯一与系统UI或API直接交互的层。
Window Driver 模式
- 这是应用驱动层中专门用于处理UI交互的部分。它为每个页面或UI组件提供一个API,将测试代码与UI的具体实现(如控件ID)解耦。
- 当UI发生变化时,只需修改Window Driver,而无需修改大量的测试用例,从而解决了UI测试脆弱的问题。
第 9 章:测试非功能性需求
第 9 章:测试非功能性需求
非功能性需求 (NFRs) 的管理
- NFRs(如性能、安全、可用性)应像功能需求一样被对待:量化、估算、排定优先级。
- “尽可能快”不是一个有效的需求。应明确为“在峰值负载下,95%的登录请求响应时间小于500ms”。
- 过早的性能优化是万恶之源。应先度量,再优化,只针对已识别的瓶颈进行优化。
容量测试 (Capacity Testing)
- 性能 (Performance): 单个事务的处理时间。
- 吞吐量 (Throughput): 单位时间内系统能处理的事务数量。
- 容量 (Capacity): 在可接受的响应时间内,系统能维持的最大吞吐量。
自动化容量测试
- 容量测试应在专用的、与生产环境高度相似的硬件上运行,以获得有意义的结果。
- 重用验收测试的场景作为容量测试的基础,可以保证测试场景的业务相关性。
- 使用录制-回放技术,捕获验收测试与API层的交互,然后放大这些交互来产生负载。
- 容量测试应该作为部署流水线中的一个独立阶段,定期运行,以尽早发现性能衰退。
第 10 章:部署和发布应用
第 10 章:部署和发布应用
发布策略
- 发布和部署的技术过程应完全相同,区别仅在于目标环境和配置。
- 制定发布策略需要开发、测试、运维等多方共同参与。
零停机发布模式
- 蓝绿部署 (Blue-Green Deployment): 准备两套完全相同的生产环境(蓝/绿)。当前线上流量指向绿色环境。将新版本部署到蓝色环境,完成测试后,将流量瞬间切换到蓝色环境。如果出现问题,可以立即切回绿色环境。这是实现快速回滚和零停机的强大模式。
- 金丝雀发布 (Canary Releasing): 将新版本只部署到一小部分服务器上(金丝雀),然后将少量用户流量(例如1%)导入新版本。通过监控金丝雀服务器的性能和业务指标,验证新版本的稳定性。确认无误后,再逐步扩大部署范围。
持续部署 (Continuous Deployment)
- 这是持续交付的自然延伸:每一个通过所有自动化测试阶段的变更都会自动地部署到生产环境。
- 它强制要求团队拥有极高质量的自动化测试、完全自动化的流程和强大的监控能力。
- 即使业务上不允许每次变更都发布,也应该以实现持续部署为目标来建设你的交付能力。
第三部分:交付生态系统
第 11 章:管理基础设施和环境
基础设施即代码 (Infrastructure as Code)
- 将基础设施(服务器、网络、防火墙等)的定义和配置以代码的形式存储在版本控制系统中。
- 使用 Puppet, Chef, Ansible 等工具来自动化地配置和管理基础设施。配置应该是声明式的(描述最终状态,而不是过程)。
- 任何对基础设施的变更都应通过修改代码和执行自动化流程来完成,严禁手动登录服务器修改。
虚拟化和云计算
- 虚拟化 (Virtualization): 使用虚拟机(VM)可以快速创建和销毁标准化的环境,是实现类生产测试环境和快速恢复的关键技术。
- 云计算 (Cloud Computing): 提供弹性的、按需付费的基础设施,极大地降低了环境准备的门槛和时间。非常适合用于并行化测试和容量测试。
监控 (Monitoring)
- 监控是交付生态系统的反馈回路。不仅要监控CPU、内存等系统指标,还要监控业务指标(如订单数、用户活跃度)。
- 行为驱动监控 (Behavior-Driven Monitoring): 使用类似验收测试的脚本来监控系统行为是否符合预期,而不仅仅是检查服务是否“存活”。
第 12 章:管理数据
第 12 章:管理数据
数据库的持续集成
- 数据库变更脚本化: 所有数据库的 schema 变更和参考数据变更都必须通过版本化的SQL脚本来管理,并存入版本控制。
- 自动化数据库迁移: 使用 DbDeploy, Flyway, Liquibase 等工具来自动化地将数据库从任意旧版本升级到目标版本。每个变更都应有对应的前滚(roll-forward)和回滚(roll-back)脚本。
零停机发布的数据库策略
- 解耦应用部署与数据库迁移: 通过让应用代码同时兼容新旧两个版本的数据库 schema,可以先部署新版应用,待稳定后再执行数据库迁移,从而避免停机。
测试数据管理
- 不要在自动化测试中使用生产数据备份: 生产数据量大、包含敏感信息,且状态不可控,不适合作为自动化测试的基线。
- 测试应该自己创建所需的数据,并在测试结束后清理。这保证了测试的独立性和可重复性。
- 对于单元测试,应完全避免访问真实数据库。
第 13 章:管理组件和依赖
第 13 章:管理组件和依赖
如何在不创建分支的情况下保持应用可发布
- 特性开关 (Feature Toggles): 使用配置文件中的开关来隐藏尚未完成或未准备好发布的新功能。代码可以持续集成到主干,但功能对用户不可见。
- 抽象分支 (Branch by Abstraction): 当需要进行大规模重构或替换一个组件时,先创建一个抽象层(如接口),让现有代码依赖这个抽象层。然后开发新的实现,完成后通过配置切换到新实现,最后移除旧实现和抽象层。
组件化与依赖关系
- 大型系统应被拆分为多个松耦合的组件。
- 组件之间应该是二进制依赖,而不是源码依赖。
- 依赖关系图应是一个有向无环图 (DAG)。应极力避免循环依赖。
组件的流水线
- 每个组件可以有自己的部署流水线。
- 当一个上游组件(如框架)构建成功后,应自动触发所有依赖它的下游组件的流水线。
- 需要一个集成流水线,它负责将所有组件的最新“良好”版本组装在一起,进行端到端的系统测试。
第 14 章:高级版本控制
第 14 章:高级版本控制
分支策略 (Branching Strategies)
- 主干开发 (Mainline Development): 这是最推荐的模式。所有人都向主干提交代码,保持代码持续集成。这是实现持续交付的基础。
- 为发布创建分支 (Branch for Release): 当准备发布时,从主干创建一个发布分支。此后,只在该分支上进行Bug修复,新功能开发仍在主干上继续。Bug修复需要合并回主干。
- 为特性创建分支 (Branch by Feature): 每个新功能都在一个独立的分支上开发,完成后再合并回主干。这种模式与持续集成理念相悖,容易导致“合并地狱”,风险很高,只在特定场景下(如开源项目)适用。
分布式版本控制系统 (DVCS)
- DVCS (如 Git, Mercurial) 与集中式系统 (如 SVN) 的核心区别是,每个开发者本地都有一个完整的仓库副本。
- 提交(Commit)是本地操作,与推送到中央仓库(Push)是分离的。这使得离线工作、在本地重写历史(rebase)成为可能。
- DVCS 极大地提升了分支和合并的效率和灵活性,但如果使用不当(如长期不推送变更),同样会破坏持续集成。
第 15 章:管理持续交付
第 15 章:管理持续交付
成熟度模型 (Maturity Model)
- 提供一个框架,帮助组织评估自身在配置管理、构建、测试、发布等方面的成熟度,并规划改进路径。
项目生命周期与风险管理
- 持续交付实践本身就是一种强大的风险管理策略。它通过缩短反馈周期、自动化、增量交付来降低发布风险。
- 项目应在早期阶段(Inception & Initiation)就规划好交付策略、测试策略和环境管理策略。
合规与审计 (Compliance and Auditing)
- 自动化优于文档: 自动化脚本是流程最准确、最可靠的记录。一个可审计的交付过程依赖于一个自动化的、可追溯的部署流水线。
- 部署流水线天然提供了强大的审计追踪能力:哪个版本的代码、由谁授权、在何时、部署到了哪个环境,所有记录一目都然。
- 通过在流水线中设置授权关卡,可以实现开发与运维的职责分离(Separation of Duties),满足合规要求。