系统架构的演变
单体应用测试实践
微服务测试的演变
服务自身的 Unit 测试
系统级的集成 (UI) 测试
Pair 集成测试
引入 Contract 概念的集成测试
CDCT(消费者驱动契约测试)
技术实践
何去何从
写在最后
系统架构的演变
伴随着互联网的快速发展, web 应用系统从面向企业内部发展到面向市场用户, 业务的日趋复杂以及用户量的上升, 那些曾经工作良好的单体应用开始遇到开发测试部署发布各个方面的瓶颈, 诸如
扩展新增功能艰难
系统庞大难以维护
编译太耗时, 发布流程太慢
等问题困扰着开发团队
SOA 的问世促使系统架构发生了跨越式的演变, 它提出了面向服务的架构思想, 将系统拆分成多个服务组件, 并通过 ESB(企业服务总线)对服务组件进行统一管理, 但重量级的 ESB 使得自身又成为了一个瓶颈随之而来的是近来业界流行的微服务架构, 它将 SOA 的思想进一步升级, 将系统组件化服务化以及去中心化, 强调 轻量级松耦合服务自治独立部署
微服务架构解决了单体应用的痛点, 打破了 SOA 的瓶颈, 同时也带来了很多的复杂性部署运维方面, 服务的部署管理监控开发设计方面, 服务的拆分设计编码测试都将会变得复杂幸运的是, 容器化技术 (比如无比流行的 Docker) 已经很大程度上帮助我们克服了环境的差异性, 而一些容器编排工具诸如 Kubernetes, Rancher, Docker-compose 提供了容器部署管理的解决方案作为行业的领航者, ThoughtWorks 也在极力倡导 开发设计部署运维一体化 的 DEVOPS 文化理念, 并通过丰富的咨询和交付成果来帮助企业研发团队更好地实施微服务架构的开发
那么在编码测试方面, 又有什么招来保证微服务架构下系统的质量?
本文将从开发测试的视角来探讨如何在微服务架构下通过不一样的测试策略来尽可能的保证系统的质量
单体应用测试实践
当我们的意识中只存在一样东西的时候, 我们便可以不假思索的拿来就用
在单体时代, 对于开发 - 测试 - 部署, 业界已经具备了一套很成熟的解决方案基于这种方案, 当一个敏捷开发的小 Team 开始构建一个应用之前, CI 搭建的过程也会变得非常简单: CI 只需要从一个代码库中去 pull 代码, 然后编译 - 测试 - 部署, 它的流程可以简化成:
在这种单线流水线模式下, 如果团队的自动化实践做得很好, 开发人员只需要关注自己编写代码时所编写的测试的质量和数量整个应用的测试策略简单直接:
保证足够的单元测试的覆盖率, 保持一定数量的 Servcie 测试, 添加一些重要业务流程的 E2E 测试
微服务测试的演变
微服务架构是一种演进式架构, 开发团队跟领域专家在一起进行业务分析(Event Storming), 从而划分出独立的服务, 系统一开始确定为独立服务的数量可能是几个, 伴随着业务的复杂深入, 会不断地衍生出新的服务下图是一个包含了四个服务的微服务架构的系统:
微服务体系中的诸多服务不可避免跨服务调用, 它们通常使用轻量级的 HTTP RESTful API 那么如何保证跨服务调用的可靠性以及整个系统集成的质量? 尤其是当不同服务由不同小团队负责开发和测试
服务自身的 Unit 测试
系统被拆分成独立的服务, 每个服务都是一个完整的小系统, 首要工作仍然是保证服务自身的业务功能的正确性比如一个 Java Web 应用(Springboot),API 功能以及各个 Service 的业务逻辑的正确性, 可以通过单元测试来保证服务细分之后从某种意义上让单元测试更加易于编写, 可以借助测试替身来屏蔽掉对其他服务依赖
系统级的集成 (UI) 测试
Unit 测试使得开发人员可以快活地活在自己的世界中, 每个开发团队按照图纸造出系统的一个部件, 只有当这些小部件集成在一起之后能够按照用户的期望为用户提供服务才体现出了系统业务价值所以我们要通过系统集成测试 (UI 测试) 来保证集成的质量
从 测试金字塔 中可以看出, 在一个系统中, UI 测试是数量最少的虽然它的业务价值最高, 但它高昂的成本使得它只会覆盖业务流程复杂的业务场景甚至, 当一个微服务架构系统中服务个数量达到一定之后, 很多开发团队对 UI 测试开始望而却步, 因为在一个存在多个服务的系统中 (即便单体应用系统) 做集成测试, 会面临诸多痛点:
需要维护完整的运行环境, 成本很高
环境不稳定 (UI 不稳定) 导致测试随机挂, 功能增强很容易破坏大量测试
问题难定位, 修复时间太长, 影响 Pipeline 的推进
运行速度慢, 反馈周期长
存在重复测试已测试的功能
这些痛点在很大程度上会削减一个开发团队的生产力, 某些企业会雇一个 QA 进行重复的人工测试从而解放开发人员的生产力这种措施有悖于追求卓越的理念, 并没有从本质上解决系统的集成的质量问题既然 UI 测试已经不适用引进了微服务架构的开发团队, 要如何保证服务集成的质量, 我们还需要在自动化测试道路上另辟蹊径
Pair 集成测试
系统级别的集成测试阻碍重重, 我们不妨退一步思考, 将集成的范围缩小 保证服务俩俩的集成的可靠性有了这个想法, 我们开始对服务俩俩配对做集成测试测试架构演变成:
我们需要真实运行待测试的服务, 并且对其他服务使用替身不难看出这种方式存在以下问题:
需要运行待集成的真实服务, 存在环境不稳定导致维护成本增加
需要 Mock 掉其他服务, 增加了额外的工作量
存在大量重复测试已经测试的功能
虽然 Pair 集成测试没有从根本上解决 UI 测试的痛点, 但它提出了 积小成多 的理念, 该理念告诉我们: 只要能够保证服务俩俩之间的集成是可靠的, 我们就可以相信系统集成也是可靠的
引入 Contract 概念的集成测试
就在两年前, 我在珠海出差的某项目上跟小伙伴一起尝试了一种集成测试方案当时项目采用的是前后端分离开发, 后端作为服务提供者提供 RESTful API, 前端作为消费者消费 API
为了保证前后端开发人员并行开展工作, 我们引入了 Contarct 概念前后端开发人员基于业务共同定义 API 协议(Contract), 该协议以 JSON 文件存在于代码库的测试资源目录中, 前端在开发过程中以 JSON 文件作为测试的断言依据而后端开发人员则参照该协议内容来实现 API
基于这种方案, 前后端开发人员如果都遵守了协议, 联调的过程就会非常顺利而它的优势也很明显的体现出来:
不需要运行其他服务, 环境简单, 运行快
测试可控范围缩小到单个服务内部
按照 Contract, 各自编写代码并测试
前后端本质上等价于服务提供方和服务消费方, 所以该理念运用在微服务之间的集成测试中, 系统的测试架构会得到进一步演进:
我么在享受着它带来的好处的同时, 问题也偷偷地潜入系统中不久后, CI 就报警了: UI 测试测试挂了
进行一番 debug 之后我们定位到了问题, 解开了
按照 Contract 单独运行测试一切 OK, 为什么上集成环境就莫名其妙挂掉!
的疑惑:
- // 两天前
- request {
- method 'POST'
- url '/users'
- body([
- name: $(regex('[a-z]{6, 20}')),
- email: 'sjyuan@thoughtworks.com',
- homePage: 'http://sjyuan.cc'
- ])
- headers {
- contentType('application/json')
- }
- }
- // 两天后
- request {
- method 'POST'
- url '/users'
- body([
- name: $(regex('[a-z]{6, 20}')),
- email: 'sjyuan@thoughtworks.com',
- homePage: 'http://sjyuan.cc',
- gender: 'M'
- ])
- headers {
- contentType('application/json')
- }
- }
通过 Git 历史记录发现服务消费方 (前端) 将 API 协议更新了, 而服务提供方 (后端) 没有同步修改实现
回顾一下
引入 Contract 概念的集成测试
, 之所以会出现协议的修改直到集成环境中才暴露出来, 是因为缺乏自动化监控机制来提前发现问题并预警让我们做进一步深入思考: 把同一份 API 契约作为服务提供方和服务消费方的测试断言依据, 一旦契约被一方改动, 则另一方的测试便会失败
归根结底, 我们缺乏一种有效的强制约束来约束双方, 马上要揭晓的
消费者驱动契约测试
可以提供这种约束
CDCT(消费者驱动契约测试)
消费者驱动契约测试的流程是, 消费者定义他们期望的 API 或消息是什么样子, 这些期望即为契约, 从这些契约可以生成存根, 此后消费者团队可以在构建过程中重复使用它们消费者和生产者都需要验证契约
CDCT 强调契约由消费者来驱动, 并由双方共同遵守, 核心是共同遵守
那么如何保证共同遵守呢?
敏捷宣言中提到
可工作的软件优于面面俱到的文档
引入 Contract 概念的测试会定义一个 Contract 文档 (JSON 协议文件) 对于消费方, 该文档被用作测试断言依据, 文档被转换成一个可工作的软件 (可执行的测试套件: 修改文档会导致测试失败) 而对于服务提供方, 因为测试的断言与 Contract 文档没有强制关联, 它最多只能是一个
面面俱到的文档
所以, 只有当双方都将文档转换成可工作的软件时, 文档的修改便会导致任意一方测试失败, 文档才真正成为双方共同遵守的契约(可工作的软件总是可靠的, 文档却有可能已经过期)
消费者驱动契约测试中存在一个契约, 双方基于契约生成可工作的测试套件:
CDCT 具备了
引入 Contract 概念集成测试
的诸多优点, 并且通过可工作的测试套件保证了契约的一致性和实时性
技术实践
运筹帷幄之中, 决胜千里之外
三国明星诸葛亮负责运筹帷幄, 关张赵等武将负责冲锋陷阵, 从而决胜千里之外的硝烟战场团队确定了测试策略之后, 应当交由优秀工具来实施执行
关于单元测试, 业界已经有非常优秀的测试工具和框架, 比如我们正在做的 Springboot 应用, JUnit, Mockito, JMock, Hamcrest 等都是测试工具箱里的明星对于 CDCT, 目前比较流行的有 JVM 框架 Spring cloud Contract, 以及支持多语言的 Pact
如果团队正在开发一个 Springboot 应用, Spring cloud Contract 是一个不错的选择它使用 Groovy DSL 定义测试契约并生成测试套件, 测试套件去验证服务提供方是否满足契约, 测试通过之后会生成一个 jar 文件, 该 jar 文件随后会作为一个可运行的 Stub server, 消费方基于 Stub server 编写测试, 从而验证功能是否满足契约:
在 CDCT 中, 不管是测试生产者还是测试消费者, 都需要引入一种快速失败方法即如果任何一方违反了契约, 最好在构建的第一分钟就失败, 而不是等到 2 小时之后的集成测试中失败所以, 我们需要将 CDCT 作为构建 Pipeline 中的一个 Stage 集成到 CI 中
何去何从
代价高昂的 UI 测试使得开发团队逐渐对它失去了信心, 尤其引入了微服务架构, 它所带来的复杂性使得业界摒弃 UI 测试的呼声高涨早在 2009 年, 著名的敏捷和 TDD 专家 J.B. Rainsberger 在 InfoQ 上提出 Integration Tests Are a Scam
集成测试是一个骗局, 你可能需要编写 2-5% 集成测试来做一个 E2E 的测试, 但它们可能到处在重复单元测, 另外集成测试存在彼此重复更糟糕的是, 当集成测试失败时, 你不知道哪里出了问题, 不能及时准确定位问题
J.B. Rainsberger 后来还在博客上发表了 Integration Tests Are a Scam, 文章借用强有力的数据分析来证实自己的观点他提出的最佳实践是: 用契约测试或协议测试来做集成测试!
Martin Fowller 在 2012 年的 测试金字塔理论 中也指出:
应该引入面向应用程序服务层的中间层测试, 这些测试既保持了端到端测试的诸多优势, 又避免了许多与 UI 框架相关的复杂性在 Web 应用程序中, 中间层测试相当于 API 层测试, 而位于金字塔顶层的 UI 测试则相当于 Selenium 测试
ThoughtWorks 技术雷达 于 2016 年已经正式采纳消费者驱动契约测试
Weve decided to bring consumer-driven contract testing back from the archive for this edition even though we had allowed it to fade in the past.
微服务架构的盛行促使越来越多的开发团队开始引入 CDCT, 逐渐淡化 UI 测试团队的测试策略正在发生不同的演变:
引入了 CDCT 并摆出了正确的姿势, 便可大大弱化 UI 测试, 甚至可以使用少量的人工测试来代替自动化 UI 测试 CDCT 帮助我们缓解了 UI 测试的痛点, 但也要当心走极端, 譬如有些团队的测试策略发生了下面的极端情况:
软件工程曾经从未产出银弹, 相信未来也不会, 一种新的方案的诞生只是解决了已有方案的痛点, 好比微服务架构解决了单体的那些痛点之后, 却又带来了足够的复杂性, 从而对团队自身的能力提出了挑战在选择测试策略的时候可以参考以下几条原则:
单元测试成本低, 运行效率高, 性价比非常高, 始终摆在第一位
高层测试只是测试防护体系的第二防线
软件开发是一项成本与收益的博弈活动, 性价比高的方案应该更加受到青睐
没有绝对的对与错, 根据自身项目工程和技术能力选择适合团队的策略
其中第二条原则强调: 如果一个高层测试失败了, 不仅仅表明功能代码中存在 bug, 还意味着单元测试的欠缺因此, 无论何时修复失败的端到端测试, 都应该同时添加相应的单元测试
写在最后
微服务架构的复杂度不仅体现在技术上, 与之相辅相成的是系统的业务架构, 而技术架构总是服务于业务架构优秀的测试策略和工程技术实践让我们更好地构建复杂的架构体系并克服它所带来的挑战, 而最终决定一个系统成功与否在于人所以, 团队中每一个人应该保持 Open 的心态, 持续学习, 提升自己的高度(技能和业务), 掌握实施微服务的相关技能, 比如利用 DDD 去做服务的划分, 从而能够更好的驾驭微服务架构
来源: https://juejin.im/entry/5a7abf4a6fb9a0634c26588d