14 分钟
持续交付——发布可靠软件的系统方法
一、软件交付的问题
1、一个简单的部署流水线
- 提交阶段
- 编译
- 单元测试
- 分析
- 构建安装包
- 自动化验收测试
- 自动化容量测试
- 手工演示、探索性测试
- 发布
2、常见反模式
- 手动部署软件
- 开发完成后才向类生产环境部署
- 生产环境的手工配置管理
3、如何实现目标
目标:快速交付
- 自动化
- 频繁做
- 无论什么修改都应接触反馈流程
- 反馈应该尽快发出
- 交付团队必须接收反馈,并根据它做出相应的行动
4、收效
- 授权团队:所有人员都能自服务,而不是昂贵的沟通
- 减少错误:可以尽早发现问题
- 缓解压力:发布变得风险低
- 部署灵活
5、候选发布版本
每次提交代码都可以产生一个可发布版本。
6、软件交付的原则
- 为软件的发布创建一个可重复且可靠的过程
- 将几乎所有的事情自动化
- 把所有的东西都纳入版本控制
- 代码
- 文档
- 配置
- 各种脚本
- 依赖
- 提前并频繁的做让你感到痛苦的事情
- 内建质量
- 测试不是一个阶段,而是贯穿软件的整个历程
- 测试不是测试人员的领域,交付团队的每个人员都应该对应用程序的质量负责
- DONE意味着已发布
- 没有完成80%的说法(估计往往不准确)
- 交付过程是每个成员的责任
- 持续改进
二、配置管理
2、使用版本控制
对所有的内容使用版本控制
- 不仅将源代码相关文件放入版本控制,还可以将系统镜像等二进制文件放入版本控制
- 不推荐将源代码编译后的二进制文件放入代码库
频繁提交代码到主干
一个矛盾:
- 满足每个提交都能通过构建,但是有些任务复杂,需要花费很长时间
- 需要频繁的提交代码
解决方法:
- 创建分支(不推荐)
- 采用增量方式开发新功能
提交代码的好的做法:
- 提交前运行测试套件
- 增量式引入变化,每完成一个小功能或重构就提交一次代码
使用意义明显的提交注释
3、依赖管理
外部库文件管理
是否将外部依赖的库文件放入版本控制(各有利弊)
组件管理
4、软件配置管理
配置与灵活性
- 高度可配置的系统代表者灵活性
- 但是“终极配置”是一种反模式,因为,修改配置信息的风险小于修改代码的风险低可能是一个错觉
- 配置信息无法像代码一样进行语法检查、编译检查
- 配置信息的正确性难以验证
- 高度可配置的系统问题
- 经常性的导致分析瘫痪
- 系统配置工作变得十分复杂,抵消了其在灵活性上带来的好处
- 应该将精力花在提供有高价值的可配置程度较低的系统上
配置的分类
- 生成二进制文件时,构建脚本可以在构建时引入相关的配置,并将其写入二进制文件
- 打包时,将配置信息一同打包到程序包中
- 在安装部署软件程序时,脚本会获取必要的配置信息
- 软件在启动或运行时可获取配置
做法
- 软件包中包含默认配置
- 在测试或运行时,覆盖默认配置
应用程序的配置管理
- 如何描述配置信息(语法)?
- 键值对的方式
- yaml
- json
- java 配置文件
- xml
- 部署脚本时如何获取这些配置信息(保存位置)?
- 数据库
- 版本控制库
- 文件目录
- 注册表
- 系统环境变量
- 配置服务器
- 在环境、应用程序、以及应用程序各个版本中每个配置信息有什么不同?
如何为配置信息建模
- 新增一个环境、可以为这个环境新增一套配置
- 创建一个程序的新版本,增减后应爱可以回滚
- 不同环境配置有效
- 数据库服务器环境迁移,应确保简单修改配置即可实现
- 通过虚拟化技术管理环境
系统配置需要进行测试
跨应用的配置管理
程序配置管理应在项目启动之初就应该考虑的内容
管理配置信息的原则
- 程序生命周期:何时注入配置,打包?部署安装?启动?运行?视情况而定
- 将配置项与源代码保存在同一代码库。但是值要保存在别处。密码不能放入版本控制库
- 应该通过自动化的方式在代码迁出时,将配置项配置好
- 配置系统应该可以根据应用、版本、环境,为打包、安装、部署脚本提供不同的配置值。每个人都能看到配置值
- 配置项使用明确的命名习惯,能自注释
- 确保配置信息的模块化和封闭性
- 不要重复
- 配置最小化
- 避免过度设计,尽可能简单
- 确保测试覆盖了配置操作
5、环境管理
避免“临时决定”,手工配置环境,要自动化的生成环境
环境配置内容如下:
- 操作系统的版本、补丁及配置设置
- 应用程序依赖的软件包
- 网络
- 外部服务及版本配置
- 现有的数据和其他先关信息如数据库初始数据
原则
- 将二进制文件和配置信息分离
- 将所有配置信息保存到一处
第三方软件的要求
- 可以通过命令行执行安装且不需要用户干预。
- 程序配置可以通过版本控制系统管理
环境管理工具
- Puppet、CfEngine
- 虚拟化
- 容器
环境变更过程管理
系统环境的变更要严肃对待,不能轻易修改,要经过严格审核。对待测试环境应当像面对生产环境一样
6、小结
好的配置管理应满足如下内容
- 仅依靠版本库中的内容就可以构建生产系统
- 可以回滚到以前某个正确状态
- 可以在测试、试运行、上线环境下以同样的方式创建部署环境
将如下内容加入变更控制
- 程序的源代码、构建脚本、测试、文档、需求、数据库脚本、代码库、配置文件
- 用于开发、测试和运维的工具集
- 用于开发、测试和生产运行的所有环境
- 应用程序的整个软件栈,包括二进制代码及相关配置
- 在应用程序的整个生命周期(构建、部署、测试及运维)的任意一种环境上,与该应用程序相关联的配置
三、持续集成
2、实现持续集成
准备工作
- 版本控制
- 自动化构建工具(命令行可运行)
- 团队共识和纪律性
持续集成工具
- CruiseControl
- Hudson
- ThoughtWorks Studios 的GO
- JetBrains的TeamCity
- Bamboo
- Pulse
使用步骤
- 检查持续构建中是否有任务,有的话等待运行完成,
- 若失败了,协助修复,然后提交自己的代码
- 一旦构建完成且测试通过,就更新自己的代码
- 在自己的机器上执行构建脚本,运行测试确保正确
- 本地构建测试
- 成功,将其提交到版本控制上
- 等待结果
- 失败,解决问题,转到第三步
- 成功,将其提交到版本控制上
3、持续集成的前提条件
- 频繁提交
- 创建全面的自动化的测试套件
- 保持较短的构建和测试过程
- 管理开发工作区,确保开发机构建测试
4、使用持续集成软件
基本构成
- Web服务器:展示构建结构和配置
- 后台进程:进行构建及测试
- 通知方式
- 独立硬件设备
- 软件插件
- 邮件等
5、必不可少的实践
- 构建失败后不要提交新的代码(保持构建通过状态)
- 提交前在本地运行所有的提交测试,或构建服务提供这个服务(预测试提交、个人构建、试飞测试)
- 等到构建测试通过后,再进行工作
- 回家之前,构建状态必须处于成功状态
- 时刻准备回滚到前一个版本
- 回滚之前确定一个修复时间
- 不要将失败的测试注释掉
- 为自己导致的问题负责
- 测试驱动开发
6、推荐的实践
- 极限编程开发实践(包括代码集体所有权)
- 若违反构架原则,就让构建失败(分清系统边界)
- 若测试运行变慢,就让构建失败(不是性能测试)
- 若有编译警告或代码风格问题,就让测试失败
7、分布式团队
- 软件开发流程不同
- 集中式持续集成
- 技术问题(网络问题)
8、分布式版本控制系统
在此系统使用持续集成,需要设定主干代码库
9、小结
- 一个巨大的可视化指示器,用于显示系统所收集的信息,以提供高质量的反馈
- 结果报告系统,以及针对自己测试团队的安装包
- 为项目经理提供的关于应用程序质量的数据提供程序
- 使用部署流水线,可以将其延伸到生产环境,为测试人员和运维团队提供一键式部署环境
四、测试策略的实现
2、测试方法的分类
常规的分类方法有两个维度
- 维度1
- 业务导向
- 技术导向
- 维度2
- 支持开发过程的
- 评判项目的
交叉起来有四种类型
- 业务导向且支持开发过程的测试——自动化功能验收测试
- 技术导向且支持开发过程的测试——自动化单元测试、集成测试、系统测试、系统测试
- 业务导向且评判项目的测试——手工的演示、易用性测试、探索性测试
- 技术导向的且评判项目的测试——手工或者自动的非功能验收测试(容量测试、安全性测试)
(1)业务导向且支持开发过程的测试
通常称为:自动化功能验收测试
在开发之前,需求初步确定阶段,可以用该测试产生需求说明文档(BDD行为驱动开发)
测试代码覆盖率达到80%可以称为全面测试
自动化的功能验收测试的好处
- 加快反馈
- 减少测试人员工作负载
- 也是回归测试的一部分
- 可以将需求分析写成测试代码
测试的维护成本可能很大
测试脚本与实现分离的工具:也就是,使用自然语言按照一定的格式进行描述功能,开发人员根据表述,提取信息绑定到测试的实现上
- Cucumber
- JBehave
- Concordion
- Twist
并非所有的内容都能自动化,但是能自动化的内容要尽量自动化
(2)技术导向且支持开发过程的测试
- 单元测试
- 依赖测试替身、模拟系统其他部分
- 不应访问数据库、文件系统、不应与外部系统交互
- 覆盖率至少80%
- 要求运行速度块
- 组件测试
- 用于测试更大的功能集合,设计IO
- 需要访问数据库、文件系统、与外部系统交互
- 可能会慢些
- 部署测试
- 测试程序是否被正常部署
(3)业务导向且评判项目的测试
通过手工检验测试如下内容
- 交付的内容是否符合用户需求
- 验证需求规格是否完美合理
方法
- 每个迭代周期结束后进行演示
- 探索性测试
- 易用性测试
- Beta测试
(4)技术导向的且评判项目的测试
主要是非功能测试
- 容量
- 安全
- 可用性
- 安全性
(5)测试替身
- 哑对象(dummy object)指被传递但是不会被使用的对象
- 假对象(fake object)真正可以被使用的,单不适用于生产环境如内存数据库
- 桩(stub)测试中为每个调用提供一个封装好的响应
- spy 是一种记录一些关于他们如何被调用的桩
- 模拟对象(mock object)编程时就设定了它预期要接收的响应。如果收到不期望的结果,将抛出异常
3、现实中情况的应对措施
(1)新项目
准备
- 选择技术平台和测试工具
- 建立一个简单的自动化构建
- 制定遵守invest原则的用户故事及考虑验收条件
- 独立的(Independent)
- 可协商的(Negotiable)
- 有价值的(Valuable)
- 可估计的(Estimable)
- 小的(small)
- 可测试的(Testable)
严格遵守以下流程
- 客户分析师、测试人员定义验收条件
- 测试人员和开发人员一起基于验收条件实现验收测试自动化
- 开发人员编码满足验收条件
- 只要有自动化测试失败,将修复工作置为高优先级
(2)项目进行时
- 选择价值高、重要的功能点进行测试自动化
(3)遗留系统
- 如果没有,就从最重要的点开始制定一个,并逐步延伸
(4)集成测试
测试系统中对外部服务有依赖的模块,如依赖第三方API等
4、流程
- 团队有好的沟通
- 每次迭代开发之前会议
管理待修复的缺陷列表
- 零缺陷,立即解决
- 缺陷列表可视化
- 确定优先级
5、小结
- 测试仅是测试的职责,更是每个人的责任
- 建立完善的反馈环
五、部署流水线
2、什么时部署流水线
指软件从版本控制库到用户手中过程的自动化表现形式。有如下阶段
提交阶段(编译、单元测试、分析、构建安装包)
|
v
验收测试阶段
|
v
用户验收测试、容量测试
|
v
生产环境
关键是足够的测试集和自动化。
部署流水线包含如下阶段:
- 提交阶段
- 自动化验收阶段
- 手工测试阶段
- 发布阶段
特点
- 自动化
- 快速可视化反馈
- 频繁执行
3、部署流水线的相关实践
- 只生成一次二进制包
- 对不同的环境采用同一部署方式
- 将平台相关性的内容通过配置服务器和环境变量切换
- 对部署进行冒烟测试
- 向生产环境的副本中部署
- 每次变更都要立即在流水线中传递
- 只要有一个环节失败,就停止整个流水线
4、提交阶段
包含如下内容
- 编译代码
- 运行一套提交测试
- 为后续阶段创建二进制包
- 执行代码分析检查代码健康状态
- 为后续阶段准备做准备工作
5、自动化验收测试之门
自动化验收测试阶段的任务主要是:自动化功能验收测试
6、后续阶段
- 手工测试
- 探索性测试
- 易用性测试
- 非功能测试
- 容量
- 安全
7、发布准备
- 让参与项目交付过程中的人共同创建并维护一个发布计划
- 尽可能的自动化测试
- 在类生产环境演练
- 意外情况有撤销发布能力
- 作为升级和撤销一部分,指定配套迁移和数据迁移策略
8、实现一个部署流水线
- 对价值流进行建模,创建一个可工作的简单框架
- 将构建和部署流程自动化
- 将单元测试和代码分析自动化
- 将验收测试自动化
- 将发布自动化
- 持续演进
9、量度
- 自动化覆盖率
- 代码库特征
- 缺陷数量
- 交付速度
- 每天提交到代码库的次数
- 每天构建次数
- 每天构建失败次数
- 每次构建所花费的时间,包括自动化测试时间
六、构建与部署的脚本化
2、构建工具概览
- Make
- Ant
- NAnt、MSBuild
- Maven
- Rake
- Buildr
- Psake
3、构建部署校本化的原则和实践
- 为部署流水线的每个阶段创建脚本
- 使用恰当的技术部署应用程序
- 使用同样的脚本向所用环境部署
- 使用操作系统自带的包管理工具
- 确保部署流程是幂等的
- 部署系统的增量式演进
4、面向JVM的应用程序的项目结构
- 项目结构:Maven项目目录结构
5、部署脚本化
- 让脚本登录到机器上,运行适当的命令集
- 写个本地脚本,在远程机器上安装一个代理,由代理从版本库中获取到脚本,并执行
部署是一种层级结构
- 应用、服务、组件 和 应用配置
- 中间件和中间件配置
- 操作系统、操作系统配置
- 硬件
测试环境的配置(比如测试数据库环境、后端服务等)
- 使用简单的冒烟测试
6、小提示
- 总是使用相对路径
- 消除手工步骤
- 从二进制包到版本控制库的内建可追溯性
- “test”不应该让构建失败
- 用集成冒烟测试来限制应用程序
七、提交阶段
2、提交阶段的原则和实践
- 提供快速有用的反馈
- 尽早发现错误
- 尽可能多的运行流水线,即使单元测试失败,也要运行接下来的测试,以获取更多的错误报告以便于一次性解决
- 何时令提交阶段失败
- 根据情况选择一个阈值(编译警告数量、测试样例失败数目等)
- 提交阶段失败,就立即停下手头工作,进行修复
- 精心对待提交阶段
- 对待构建脚本、运行脚本要像对待代码一样
- 脚本要不断的演进优化
- 让开发人员也拥有所有权
- 开发人员可以看懂并理解和控制构建流水线
- 开发人员和维护人员应该对构建系统维护并负责
- 在超大项目中指定一个构建负责人
- 提交阶段的结果
3、提交阶段的结果
提交阶段的输入是源代码,输出如下
- 二进制包
- 报告
- 测试结果
- 代码库分析报告
- 测试覆盖率
- 圈复杂度
- 复制粘贴分析
- 输入输出耦合度
制品库
- 用于存放最终可运行的部分二进制包和结果报告的仓库
- 应该与版本库关联,当二进制包被删除应可以根据源代码库重新生成
- 制品库应该提供RESTful接口
- 可以使用类似Maven的工具
- 提交阶段根据源代码生成二进制包,并放入制品库
- 在后续的步骤(验收测试、手工测试、性能测试、发布阶段)的输入都是制品库中的二进制包
- 在发布之后将该二进制包标记为已发布
4、提交测试套件的原则与实践
- 大部分是单元测试
- 运行速度快
- 代码覆盖率达到80%
- 使用mock和stub,提高速度
- 避免用户界面
- 不要将提交阶段的单元测试设置在用户界面
- 将用户界面测试放入验收测试阶段
- 使用依赖注入
- A依赖于B,使用以来注入后,在更改B的实现,A的代码不需要改变即可实现变更
- 因此,可方便的在测试A时,创建一个mock B
- 避免使用数据库
- 单元测试避免使用异步
- 使用测试替身(mock)(mock和stub区别是:mock是自动生成一个代理、stub可能需要手写)
- Mockito
- Rhino
- EasyMock
- JMock
- NMock
- Mocka
- 最少化测试中的状态
- 时间伪装
- 系统中所有使用系统时间的地方使用一个获取时间的封装,这样就可以使用mock技术进行测试
- 提交测试花费的事件最好在5分钟内,不能超过10分钟
八、自动化验收测试
- 功能性验收测试
- 非功能性验收测试
- 容量
- 性能
- 可修改性
- 可用性
- 安全性
- 易用性
与单元测试的区别是:验收测试是针对业务的
2、为什么验收测试是十分重要的
- 手工测试对复杂项目成本很高,且限制了频繁发布
- 只有验收测试才能证明软件对用户的价值,测试的是用户场景
- 能检测出线程问题(死锁)、架构问题、环境问题
- 可以保证程序发生大规模修改后是否仍然正确
- 没有验收测试,测试人员的负担会十分重
(1)如何创建可维护的验收测试套件
- 注重分析过程,验收测试来源于验收条件
- 遵循INVEST原则
- 验收测试应该是分层的
- 第一层是:验收条件
- 第二层是:测试实现层
- 使用领域语言实现测试,
- 不能是与内部API和UI耦合
- 第三层是:应用程序驱动层
- 理解如何与应用程序进行交互,来执行一系列动作,
(2)GUI上测试
- 如果GUI仅是展示数据,不包括逻辑(好的设计),可以绕过界面直接在业务逻辑层进行测试
3、创建验收测试
(1)分析人员和测试人员的角色
分析人员:
- 代表客户和用户
- 和客户工作:识别需求,排定优先级
- 和开发人员工作:确保开发人员理解需求
- 和测试人员工作:确保验收条件被合理阐释
测试人员: 。。。
(2)迭代开发项目中的分析工作
(3)将验收条件变成可执行的规格说明书
行为驱动开发。验收测试的特定语言:
- 假如(Given)初始条件
- 当(When)某个事件发生时
- 那么(Then)就会有什么结果
4、应用程序驱动层
是知道如何与应用程序打交道的层次。应用程序驱动层所用的API是以某种领域语言的表达。
(1)如何表达验收条件
- 外部DSL和程序API各有优缺点
(2)窗口驱动模式
让测试与GUI解耦
5、实现验收测试
(1)测试中的状态
- 要是状态依赖最小化
- 维护最小依赖数据集
- 利用功能间的隔离性进行测试
(2)过程边界、封装和测试
(3)管理异步和超时问题
(4)使用测试替身对象
5、验收测试阶段
验收测试在高层,错误可能并不是被测方的问题。所以可以进行测试录像。以方便查找失败原因
- 确保一致处于通过状态
- 部署测试:尽量与生产环境一致
6、验收测试性能
提高性能方法
- 重构通用任务
- 共享昂贵资源
- 并行测试
- 使用计算网格