设计完成后,开发人员就开始编码了,测试人员写详细的测试用例 或基于 API 接口的自动化测试。编码涉及到 git 分支如何控制,是否要先写单元测试,是从上到下,还是从下到上的编码,如何前后端联调,代码是否要检视,如何检视,如何尽可能早地测试。
代码管理
编码之前,先要做好代码版本管理。现在主流的就是 git 版本管理,git 工作流有很多,网上有很多相关资料, 但大体上可以分为两种,一种是建立特性分支,当该特性完全开发完成再合并到主分支,另一种就是在主分支上开发,保证每天主分支是可发布的。这里说的主分支不一定是 master,可以是 develop,到该迭代周期结束时,测试没有问题,再同步到 master。特性分支这种适合周期性交付,并且各个团队所负责的特性耦合度低,最后合并时需要解决冲突很少,没有单元测试的比较适合。主分支开发这种更加敏捷,能够每天做到可以运行版本,更符合敏捷的思想,更适合多个开发团队负责的功能模块具有一定耦合性,这样多个团队每天合并代码,就能早发现问题早解决。今天刚出的问题大家对代码很熟悉,而且出问题原因也只局限于今天的代码,也就容易排查解决。对比特性分支那种在最后交付才发现问题,涉及排查定位范围更广,问题更多,大家代码熟悉度有所下降, 风险和成本显著增加。不过,主分支每天开发提交,需要完整的自动化测试用例和单元测试来保障,这样才能做到每天测试,快速及时发现合并问题,否则,没有这些自动化测试来保障,那么主干分支的各种问题不能及时发现,导致各个团队之间代码相互影响,频繁出现各种未知问题,从而大大降低开发效率,这样还不如特性分支方式的效率高, 所以自动化测试和单元测试是关键。如果项目规模不是很大,并且能够保证高质量的自动化测试和单元测试,那么强烈建议 git 主分支开发方式。
很多软件产品,面向不同客户,都会有一些定制开发,这些定制功能的代码管理就成了一个难题。现在的项目多数都是微服务,一个软件项目会有很多子项目,定制功能一般只涉及部分子项目,对这部分子项目肯定会建立对应该客户的 git 分支,但剩余不涉及代码变动子项目怎么办? 如果这些不变动的子项目采用主分支,不对客户的作区分,会无法确定客户的线上代码对应的主分支哪个历史版本,主分支经常迭代开发,但线上客户的不一定能跟随升级。还有客户线上不一定部署所有功能,可能部分功能对应子项目并没有上线,也就没有办法直观地加以区分,所以不涉及变动的子项目采用主分支是不可行的。因此,我们自然想到把客户线上对应的不涉及到定制功能的子项目的代码也加上新分支,这样就解决上面说的问题,线上升级了,就和主分支代码合并一次,线上没有部署的功能子项目也没有对应的代码分支,区分也很明显。不过,这样没法直观区分哪个子项目的代码涉及到定制,也不方便一眼看出该客户所涉及到所有子项目。还有一个问题是,客户定制功能代码只有一个分支名的话,怎么区分正在修改开发分支和线上正在运行的分支,如果对定制功能的子项目都建立两个分支,也要建立不少。为了解决上面的问题,可以采用 git submodule 技术来管理,这样就把所有该客户涉及到的子项目都聚合在一起,也无需对非定制功能的子项目建立分支,而且还能对整个 submodule 创建 develop 和 master 等整体分支,一致性也更好的,不过 git submodule 技术也更加复杂。因此,在一个产品有多个客户的情况下,并且有不同的定制功能,那么采用 git submodule 技术来管理代码往往是最适合的。
单元测试
在正规软件开发中,单元测试是必需的。但在国内很多不靠谱的项目里,资源压缩很厉害,时间很紧急,要求也是基本能够演示或可用就行,生命周期也非常短,这种情况下显然不会给你时间写单元测试,当然也不值得。比较靠谱的项目,还是必须要做单元测试的,从而保证项目的每一步扎扎实实,开发出稳定、可靠、易维护的软件。
单元测试不仅是为了尽早测出编码中的问题,还能从使用者的角度尽早调用单元接口,来审视是否好用、完备和可测性,从而尽早地改善单元的接口设计。因此单元测试代码编写必须在业务编码之前,通过单元测试的编写,优化单元的接口和设计,考虑清楚关键指标,再开始编写业务代码,这样比那些先编码后单元测试的开发质量更高,问题更少,效率也就更高。而实际中,往往却有很多人先编码后写单元测试,他们担心接口在实现时发现问题需要变动,导致部分测试用例返工,也担心时间紧张,先保证编码完成再考虑单元测试,当然还有习惯使然。常规业务代码开发应该从使用角度去思考,而不是从实现角度去想,这样设计的接口更加好用,如果有啥技术问题可以先做技术验证,所以担心实现而引起变动不是很有必要,一般业务类型代码基本很少出现实现不了的问题。开发的时间不要安排的紧张,大家才能愿意去认真严格的先写测试用例,不用过多的担心任务能否按时完成,较为宽松的环境人们才愿意改变习惯。
单元测试开发时的注意事项。单元测试代码可以先写该单元模块的几个接口的测试用例,再编码实现这些接口,然后再跑测试用例,没必要全部写完单元测试再写实现。单元测试的代码质量要和业务代码质量一样高,而且都要进行代码检视和后期的同步维护,随着接口调整或者代码重构,单元测试的代码也要随之调整优化。 单元测试中一个测试用例只能测试一种情况,这样才能快速定位出问题,否则,还需调测排查,花费更多的时间定位问题, 有违单元测试的目标。依赖中间件或耦合的模块,尽量采用 mock 模拟数据,排除外部干扰,更好专注于本单元模块的代码测试。如果单元测试时需要依赖数据库,重点不是特定数据的测试,那么可以用 H2 这种内存数据库替换,减少依赖性,避免了数据还原。如果是像半结构化数据的数据库,尽量能mock,避免不了那么就一个用例跑完后,还原一次,还原方法一般采用脚本重置数据或 代码调用 API 修改回默认数据。单元测试对那些很简单的业务逻辑的代码就没有必要去写,例如,只是简单设置值和取值。每个测试用例必须是独立的,和其他用例没有任何依赖性,也必须是可重复的,无论重复多少次,结果是相同的。如果要想用例并行,那么需要用例之间的数据隔离,从而避免相互影响。单元测试的代码目录结构和业务代码的目录结构基本对应,方便业务模块找到对应的单元测试用例。单元测试的粒度足够小,有利于精确定位一个问题,一般粒度是每个方法或函数,最大不能超过一个类。
单元测试是保证项目质量的重要基石,但它的开发和维护成本很高,一般是业务代码的工作量的一倍以上,不过得到的收益也是巨大的,只有确保每一步的稳定可靠,才能支撑起这么复杂的软件世界。
编码
编码就像说话写作一样,都是为了传达沟通,不同的是编码不仅是对机器的沟通,还有人和人的沟通。机器执行你的代码,不会在乎你写的代码是否优美简洁还是晦涩难懂,只要能编译成功,它都能顺利执行。如果代码要让人们能够清晰易懂和易维护,就需要在编码时精心设计、遵从编码规范。国内大多数的程序员的编码的目标只是能够正确实现功能就行,对代码可读性和可维护性的关注就很少,更多的精力会花在学习实践新技术,例如、数据库、中间件、框架库等各种工具上面。我认为编码质量和各种技术框架库、中间件,对应的就是编程的内功和外功,许多程序员的编码质量只要达到可运行,就基本不怎么努力提高了。 一方面是内功上面的努力越来越困难,短时间内很难有质的提升,另一方面不像外功那样立竿见影,会在开发效率和能力上有显著的提升,况且很多公司的项目也不怎么在乎代码质量,只要能快速实现功能就行。这样下去,程序员随着时间积累(一般超过 3 年)编程能力不再有明显的提升,从而也很难胜任中大型软件项目中的质量要求,也就达到自己职业和收入的瓶颈。只有不断提高自己的内功,不停探索如何写出高质量的代码,设计出更好的结构,掌握更多的设计模式和编程规范,这样不仅能写出更高质量的代码, 也能掌握架构设计的能力,从而职业发展和收入越来越好。如何判断自己的代码质量是否有所成长?你在写代码时是按部就班、不加思考的机械化本能式编程,还是抓耳挠腮,不断思考如何命名,如何划分结构,如何实现各种编程规范,消除全部 静态检测代码的警告,对代码经常进行推敲优化,如果是后者,就表明你是不断在代码质量上努力。当你回看几个月前你写的代码,感觉很垃圾,证明你的进步挺快。 还有当你写的代码,当有相关背景的业务背景知识的人,看你的代码时,不需要询问你,就能看懂,也证明代码质量很好,达到了自注释的要求 。 如果要保证整个团队的代码质量,就必须通过检视来完成。
编码需要不断地重构优化。随着业务知识了解的深入、技能的提高、需求的变化,都会促使我们重构优化原有的代码,使之更能满足新的需求。没人一开始就能写出高质量代码,也没有人能预计到业务发展的具体变化,不可能刚开始写的代码就能适合后续的发展。为了保证后续的业务易扩展维护,就必须得在开发新功能之前,重构优化原有相关地方,使之整个项目保持敏捷性,不会随着时间积累,新功能开发成本越高,可维护性越差。重构时单元测试就显得尤为重要,避免重构引发各种新问题,从而使得人们乐于重构。不仅需要对业务代码重构,还需要对测试代码重构,保证测试代码简洁清晰。重构实践具体指导,可以参考经典书籍《重构:改善即有代码的设计》。
编码的顺序。越靠上层越接近业务,越靠下层越接近底层,那么编码时应该从上到下来实现,还是从下到上?常规业务性开发,往往是从上到下来实现,能够从使用角度更好的定义下层的接口,也容易让下层为上层支持和服务,不容易脱离业务目标,从而更能保证整体业务实现的准确性和易用性。这种方式带来风险,底层实现的技术风险加大,怕万一不能或很难实现上层的需求,不过常规的业务开发,技术实现一般都不是难事。如果是那种探索研究型的项目,或者底层依赖的风险较高的第三方的那种,那么就从下往上的开发最为合适,从而早发现问题早解决,或者调整上层业务来避免。一般初级程序员,对自己的开发能力没有信心,往往会自发地从下到上地开发,即使后面能力成长上来了,还是会按照惯性继续从下到上开发,最好还是在刚开始就要养成从上到下的习惯。从上到下的编码,也不是把本次业务上层全部做完,再去实现下一层,而是把某个接口从上到下的实现,实现完这个接口,再实现另一个接口,依次从上到下的实现,也就是常说的端到端的实现。这种方式,早实现部分功能,从而能够早交付,提高了可用性和敏捷性。编码的顺序还有很多种,具体视情况不同,更多的请参考《代码大全 2》。
测试
在微服务开发测试当中, 除了上面提到的单元测试,还有契约测试、集成测试、组件测试及端和端测试,每个测试的重点都不一样,每个项目的资源和特点不同,也就没有必要把各个测试阶段都做。
契约测试主要是测试微服务之间的契约接口是否满足,如果更新变化,能够及时发现所影响到的消费方。它主要关心接口的格式是否符合,不关心接口的内容在业务意义上的正确性。例如,某接口定义了返回内容的年龄字段为整型,契约测试只是检测返回的年龄字段是否存在、字段值是否为整型,而不是检测年龄字段值是否符合业务,即使把年龄的字段值写成 10000,契约测试也是通过的。从上面可以看出,有了契约测试,能够更好的保持接口的稳定性和兼容性,减少生产者接口变化而导致的消费方的异常。契约测试的主要框架有Pact 、spring cloud Contract,推荐使用 Pact,除了支持更多的语言外,还能更好的测试出是哪个消费者不兼容 。契约测试成本也不小,如果你的项目不算大,服务之间依赖也不多,接口变化不多,就没必要进行契约测试了,可以通过其他的成本更低的方式间接地达到契约测试的目标。 例如,在升级某个接口时,尽量符合开闭原则,这样就能做到向后兼容,如果不能,则设计一个新接口,并把该接口版本号加 1,并保留老接口一段时间,再通知消费方的服务的接口人,说明情况,告诉老接口的废除日期,当大家都修改完了,并通过一两轮的测试发布,再删除原先的弃用的接口。这样对很多中小公司的多数微服务软件项目够用,但成本要比契约测试低得多。
集成测试主要目标测试各个服务的连通性,不关注业务上的正确性。集成测试是在各个组件、数据库和其相关微服务运行的情况下,来测试该服务的接口,看看接口调用是否异常,整个调用链和数据库、中间件等是否正常。和契约测试类似,但不同的是集成测试是真实环境,而契约测试的微服务的外部都是模拟的,对数据库、中间件和相关其余服务都是通过 mock 模拟的。因此,集成测试更加全面,整个系统联通性测试更好。多数项目的集成测试在集成手动来测,不必要自动化,降低成本。
组件测试是对一个子系统的业务功能上的测试,也包括性能和安全的相关测试。组件测试与端到端的接口自动化测试的目标相同,只是对用规模不同,组件测试只是对某个子系统的测试,而接口自动化测试是对整个系统的测试。当项目很大需要分别划分多个子系统同时开发时,组件测试更为适合,先保证各个子系统的业务功能正常,然后再合并到整个系统。组件的大小根据子系统的规模来定,最小是一个微服务。组件测试所需的外部依赖通过模拟来完成。组件测试主要就是基于接口的自动化测试,一般由测试开发工程师来完成。
端对端测试就是对整个系统的测试,从用户端到系统端。端对端分为手工测试和自动化测试,常规性的业务功能测试都是自动化测试,手工测试适合探索性测试,或者临时部分功能的测试。自动化测试又分为 UI 测试和基于后端面向 UI 的 API 测试。 UI 变动大,所以只对关键性并且相对稳定的 UI 部分进行测试,一般先录屏,然后再修改方式来实现,这样效率高成本低。API 自动化测试,也是对关键功能,系统业务的测试,测试覆盖率比 UI 测试要高,但和单元测试相比,就要低很多。端对端的测试复杂性高,依赖多,最好对特定功能建立好预制数据,测试用例开发复杂度要降低很多。
整个测试阶段都是金字塔模型,底层是单元测试,最上层是端对端测试,每一层是对下一层的补充。最底层的单元测试,覆盖率最高,隔离性最好,测试开发量最大,定位问题速度最快,而最上一层端对端就要求的覆盖率低,把经常使用且稳定的关键功能进行自动化测试,覆盖率也就最低,测试时间长,发现的问题定位速度慢。整个测试阶段根据项目特点,资源情况进行取舍,多数项目做好单元测试和端对端测试就够了。
联调
现在的主流架构都是微服务,一般业务调度的流程很长,特别还有许多异步调用,而且负责的人员很多,联调中发现问题,就需要涉及多方的合作,才能定位解决问题。
联调时涉及多方的定位解决流程。一般发现问题,自己先定位问题,如果定位到是自己的问题,那就直接解决。如果不是,发现的是自己依赖别的人负责的模块,那么找到这个人说明具体情况,再继续由他来定位解决,如果他定位到的问题也是他所依赖的别的模块导致的,那么他再找该模块的负责人,依此类推,直到定位发现并解决根本问题后,再按照定位时的路径,依次反馈并进行复测。不要出现让第一个发现问题的人,去不断联系协调所有依赖路径的相关人来定位解决,这样会导致第一个人员工作量增加,打击积极性,从而对那些不是很严重的问题进行隐藏,不利于项目的整体质量。这种职责传递的方式,不仅降低了第一个人的工作量,而且在沟通上也更加清晰明确。
像微服务架构的问题定位,可以通过跟踪链来快速解决,可以较为容易地定位到是哪个服务返回的数据有问题,从而直接跨过中间层级,直接找到问题节点,协调对方来解决。