微服务中的测试

新的挑战 微服务和传统的单块应用相比,在测试策略上,会有一些不太一样的地方。简单来说,在微服务架构中,测试的层次变得更多,而且对环境的搭建要求更高。比如对单块应用,在一个机器上就可以setup出所有的依赖,但是在微服务场景下,由于依赖的服务往往很多,要搭建一个完整的环境非常困难,这对团队的DevOps的能力也有比较高的要求。 相对于单块来说,微服务架构具有以下特点: 每个微服务在物理上分属不同进程 服务间往往通过RESTful来集成 多语言,多数据库,多运行时 网络的不可靠特性 不同的团队和交付周期 上述的这些微服务环境的特点,决定了在微服务场景中进行测试自然会面临的一些挑战: 服务间依赖关系复杂 需要为每个不同语言,不同数据库的服务搭建各自的环境 端到端测试时,环境准备复杂 网络的不可靠会导致测试套件的不稳定 团队之间的沟通成本 测试的分层 相比于常见的三层测试金字塔,在微服务场景下,这个层次可以被扩展为5层(如果将UI测试单独抽取出来,可以分为六层)。 单元测试 集成测试 组件测试 契约测试 端到端测试 和测试金字塔的基本原则相同: 越往上,越接近业务/最终用户;越往下,越接近开发 越往上,测试用例越少 越往上,测试成本越高(越耗时,失败时的信息越模糊,越难跟踪) 单元测试 单元测试,即每个微服务内部,对于领域对象,领域逻辑的测试。它的隔离性比较高,无需其他依赖,执行速度较快。 对于业务规则: 商用软件需要License才可以使用,License有时间限制 需要License的软件在到期之前,系统需要发出告警 @Test public void license_should_expire_after_the_evaluation_period() { LocalDate fixed = getDateFrom("2015-09-03"); License license = new License(fixed.toDate(), 1); boolean isExpiredOn = license.isExpiredOn(fixed.plusYears(1).plusDays(1).toDate()); assertTrue(isExpiredOn); } @Test public void license_should_not_expire_before_the_evaluation_period() { LocalDate fixed = getDateFrom("2015-09-05"); License license = new License(fixed....

October 1, 2016 2 min

敏捷团队里的QA

QA的职责 我们先讨论一下传统的瀑布模型下QA是如何工作的,其中最主要的问题是什么;然后作为对比,我们来看看在敏捷团队里QA又是如何工作的,工作重点又是什么;最后,我们详细看一看在新的职责下,QA应该如何做。 瀑布开发模型 即使在今天,在很多企业中,瀑布模型仍然是主流。每一个需求都需要经过分析,设计,开发,测试,上线部署,运维等阶段。虽然一些企业已经在实施敏捷开发,比如项目/产品以迭代的方式运作,也有诸如每日站会,代码检视等敏捷实践,但是如果仔细审视,你会发现其实开发模式骨子里还是瀑布:按照软件组件划分的部门结构(详见康威定律),按照职能划分的团队(开发和测试分属不同部门),过长的反馈周期,永远无法摆脱的集成难题等等。 随着软件变得越来越复杂,团队里没有任何一个人可以说出系统是如何运作的,也不知道最终用户是谁,以及最终用户会以何种方式来使用最终的软件。 更糟糕的是,按照职能划分的团队在物理上都是隔离的,比如独立的测试部门,独立的运维部门,整日忙碌而难以预约到档期的业务人员,当然还有经常疲于交付,无处吐槽的苦逼开发。由于这些隔离,信息的反馈周期会非常长,一个本来很容易修复的缺陷可能在4周之后才可能被另一个部门的测试发现,然后通过复杂的工作流(比如某种形式的缺陷追踪系统)流到开发那里,而开发可能还在拼命的完成早就应该交付的功能,从而形成恶性循环。 瀑布模式中的QA 在这样的环境中,QA们能做的事情非常有限。在需求开始时会他们参加需求澄清的会议,制定一些测试计划,然后进行测试用例的设计。有的企业会用诸如Excel之类的工具来记录这些用例。这些写在Excel里的,死的用例用处非常有限。而最大的问题在于:它们无法自动化执行。另外,在实际软件开发中,需求总是会经常发生变化,需求的优先级也会有调整,然后这些记录在Excel中的死的用例会很快过期,变得无人问津。 除此之外,QA中的有些成员会使用工具来录制一些UI测试的场景,然后在每个新版本出来之后进行回放即可。然而,当UI发生一点变化之后,这些自动化的用例就会失效:比如HTML片段中元素位置的调整,JavaScript的异步调用超时等等。 显然,这种单纯以黑盒的形式来检查功能点的测试方式是不工作的,要真正有效的提升软件质量,仅仅通过事后检查是远远不够的,软件的质量也应该内建于软件之中。QA的工作也应该是一个贯穿软件生命周期的活动,从商业想法,到真实上线,这其中的所有环节,都应该有QA的参与。 系统思考 如果不从一个系统的角度来思考软件质量,就无法真正构建出健壮的、让业务和团队都有信心的软件系统。质量从来都不只是QA的职责,而是整个团队的职责。 关于软件质量,一个根深蒂固的误解是:缺陷在开发过程中被引入,然后在测试阶段被发现,最后在QA和开发的来来回回的撕扯中被解决(或者数量被大规模降低),最后在生产环境中,就只会有很少的,优先级很低的缺陷。 然而事实上,很多需求就没有仔细分析,业务价值不很确定,验收条件模糊,流入开发后又会引入一些代码级别的错误,以及业务规则上的缺陷,测试阶段会漏掉一些功能点,上线之后更是问题百出(网络故障,缓存失效,黑客攻击,操作系统补丁,甚至内存溢出,log文件将磁盘写满等等)。 在一个敏捷团队中,每个个人都应该对质量负责,而QA则以自己的丰富经验和独特视角来发掘系统中可能的质量隐患,并帮助团队将这些隐患消除。 我在ThoughtWorks的同事Anand Bagmar在他的演讲What is Agile testing- How does automation help?中详细讨论过这部分内容。 QA到底应该干什么? 本质上来说,任何软件项目的目标都应该是:更快地将高质量的软件从想法变成产品。 将这个大目标细分一下,会得到这样几个子项,即企业需要: 更多的商业回报(发掘业务价值) 更快的上线时间(做最简单,直接的版本) 更好的软件质量(质量内嵌) 更少的资源投入(减少浪费) 其实就是传说中的多、快、好、省。如果说这是每一个软件项目的目标的话,那么团队里的每一个个人都应该向着这个目标而努力,任何其他形式的工作都可以归类为浪费。用Excel记录那些经常会失效,而且无法自动执行的测试用例是浪费,会因为页面布局变化而大面积失效的UI测试也是浪费,一个容易修复的缺陷要等到数周之后才被发现也是浪费。 在这个大前提下,我们再来思考QA在团队里应该做什么以及怎么做。 QA的职责 Lisa Crispin在《敏捷软件测试》中提到过一个很著名的模型:敏捷测试四象限。这个模型是QA制定测试策略时的一个重要参考: 如果按照纵向划分的话,图中的活动,越向上越面向业务;越向下越面向技术。横向划分的话,往左是支撑团队;往右是评价产品。 其实简化一下,QA在团队里的工作,可以分为两大类: 确保我们在正确的交付产品 确保我们交付了正确的产品 根据这个四象限的划分,大部分团队可能都会从Q2起步:QA会和BA,甚至UX一起,从需求分析入手,进行需求分析,业务场景梳理,这时候没有具体的可以被测试的软件代码。不过这并不妨碍测试活动,比如一些纸上原型的设计(感谢刘海生供图): 通过这一阶段之后,我们已经有了用户故事,这时候QA需要和开发一起编写用户故事的自动化验收测试。当开发交付一部分功能之后,QA就可以做常规的用户故事测试,几个迭代之后,QA开始进行跨功能需求测试和探索性测试等。根据探索性测试的结果,QA可能会调整测试策略,调整测试优先级,完善测试用例等等。 根据项目的不同,团队可以从不同的象限开始测试策略的制定。事实上,Q1-Q4仅仅是一个编号,与时间、阶段并无关系,Lisa Crispin还专门撰文解释过。 关于QA如何在软件分析的上游就介入,然后通过BDD的方式与业务分析师一起产出软件的各种规格描述,并通过实例的方式来帮助整个团队对需求的理解,ThoughtWorks的林冰玉有一篇文章很好的介绍了BDD的正确做法。如果将QA的外延扩展到在线的生产环境,制定合理的测量指标,调整测试策略,强烈推荐林冰玉写的另一篇文章产品环境中的QA。 其他职责 事实上,软件生命周期中有很多的活动,有很多处于灰色地段。既可以说是应该开发做,又可以说应该QA做,甚至可以推给其他角色(比如OPs)。不过我们知道,一旦涉及角色,人们就再也不会按照全局优化的思路来应对问题了。这种灰色的活动包括: 持续集成的搭建 测试环境的创建于维护 UAT上的数据准备 代码中的测试代码的维护 测试代码的重构 在团队实践中,这些活动我们通常会让QA和开发或者OPs同事一起结对来完成。一方面避免知识孤岛的形成,另一方面在跨角色的工作中,也可以激发出更多不同的思路。 万能的QA? 虽然在这些活动中,QA都会参与,但是并不是说团队里只要有一个QA就可以了。QA在参与这些活动时,侧重点还是有很大不同的。 比如需求分析阶段,如果有QA的加入,一些从QA角度可以发现的有明显缺陷的场景,则可以在分析阶段就得到很好的处理。另一方面,尽早介入可以设计出更合理的测试计划(比如哪些功能的优先级比较高,用户更会频繁使用,那么对应的测试比重也会更高)。在Story分析与书写阶段,QA可以帮助写出更加合理的验收条件,既满足业务需求,又可以很好的指导开发。 在和开发一起编写澄清需求时,主要是编写自动化验收测试,而不是实际编写业务逻辑的实现(虽然QA应该参与Code Reivew环节,学习并分享自己的观点);甚至在上线运维阶段,QA还需要和OPs一起来设计用户数据的采集指标(比如用户访问的关键路径,浏览器版本,地区的区分等),从而制定出新的测试策略。 扩展阅读 What is Agile testing - How does automation help?...

September 24, 2016 1 min

前后端分离了,然后呢

前言 前后端分离已经是业界所共识的一种开发/部署模式了。所谓的前后端分离,并不是传统行业中的按部门划分,一部分人纯做前端(HTML/CSS/JavaScript/Flex),另一部分人纯做后端,因为这种方式是不工作的:比如很多团队采取了后端的模板技术(JSP, FreeMarker, ERB等等),前端的开发和调试需要一个后台Web容器的支持,从而无法做到真正的分离(更不用提在部署的时候,由于动态内容和静态内容混在一起,当设计动态静态分流的时候,处理起来非常麻烦)。关于前后端开发的另一个讨论可以参考这里。 即使通过API来解耦前端和后端开发过程,前后端通过RESTFul的接口来通信,前端的静态内容和后端的动态计算分别开发,分别部署,集成仍然是一个绕不开的问题 — 前端/后端的应用都可以独立的运行,但是集成起来却不工作。我们需要花费大量的精力来调试,直到上线前仍然没有人有信心所有的接口都是工作的。 一点背景 一个典型的Web应用的布局看起来是这样的: 前后端都各自有自己的开发流程,构建工具,测试集合等等。前后端仅仅通过接口来编程,这个接口可能是JSON格式的RESTFul的接口,也可能是XML的,重点是后台只负责数据的提供和计算,而完全不处理展现。而前端则负责拿到数据,组织数据并展现的工作。这样结构清晰,关注点分离,前后端会变得相对独立并松耦合。 上述的场景还是比较理想,我们事实上在实际环境中会有非常复杂的场景,比如异构的网络,异构的操作系统等等: 在实际的场景中,后端可能还会更复杂,比如用C语言做数据采集,然后通过Java整合到一个数据仓库,然后该数据仓库又有一层Web Service,最后若干个这样的Web Service又被一个Ruby的聚合Service整合在一起返回给前端。在这样一个复杂的系统中,后台任意端点的失败都可能阻塞前端的开发流程,因此我们会采用mock的方式来解决这个问题: 这个mock服务器可以启动一个简单的HTTP服务器,然后将一些静态的内容serve出来,以供前端代码使用。这样的好处很多: 前后端开发相对独立 后端的进度不会影响前端开发 启动速度更快 前后端都可以使用自己熟悉的技术栈(让前端的学maven,让后端的用gulp都会很不顺手) 但是当集成依然是一个令人头疼的难题。我们往往在集成的时候才发现,本来协商的数据结构变了:deliveryAddress字段本来是一个字符串,现在变成数组了(业务发生了变更,系统现在可以支持多个快递地址);price字段变成字符串,协商的时候是number;用户邮箱地址多了一个层级等等。这些变动在所难免,而且时有发生,这会花费大量的调试时间和集成时间,更别提修改之后的回归测试了。 所以仅仅使用一个静态服务器,然后提供mock数据是远远不够的。我们需要的mock应该还能做到: 前端依赖指定格式的mock数据来进行UI开发 前端的开发和测试都基于这些mock数据 后端产生指定格式的mock数据 后端需要测试来确保生成的mock数据正是前端需要的 简而言之,我们需要商定一些契约,并将这些契约作为可以被测试的中间格式。然后前后端都需要有测试来使用这些契约。一旦契约发生变化,则另一方的测试会失败,这样就会驱动双方协商,并降低集成时的浪费。 一个实际的场景是:前端发现已有的某个契约中,缺少了一个address的字段,于是就在契约中添加了该字段。然后在UI上将这个字段正确的展现了(当然还设置了字体,字号,颜色等等)。但是后台生成该契约的服务并没有感知到这一变化,当运行生成契约部分测试(后台)时,测试会失败了 — 因为它并没有生成这个字段。于是后端工程师就找前端来商量,了解业务逻辑之后,他会修改代码,并保证测试通过。这样,当集成的时候,就不会出现UI上少了一个字段,但是谁也不知道是前端问题,后端问题,还是数据库问题等。 而且实际的项目中,往往都是多个页面,多个API,多个版本,多个团队同时进行开发,这样的契约会降低非常多的调试时间,使得集成相对平滑。 在实践中,契约可以定义为一个JSON文件,或者一个XML的payload。只需要保证前后端共享同一个契约集合来做测试,那么集成工作就会从中受益。一个最简单的形式是:提供一些静态的mock文件,而前端所有发往后台的请求都被某种机制拦截,并转换成对该静态资源的请求。 moco,基于Java wiremock,基于Java sinatra,基于Ruby 看到sinatra被列在这里,可能熟悉Ruby的人会反对:它可是一个后端全功能的的程序库啊。之所以列它在这里,是因为sinatra提供了一套简洁优美的DSL,这个DSL非常契合Web语言,我找不到更漂亮的方式来使得这个mock server更加易读,所以就采用了它。 一个例子 我们以这个应用为示例,来说明如何在前后端分离之后,保证代码的质量,并降低集成的成本。这个应用场景很简单:所有人都可以看到一个条目列表,每个登陆用户都可以选择自己喜欢的条目,并为之加星。加星之后的条目会保存到用户自己的个人中心中。用户界面看起来是这样的: 不过为了专注在我们的中心上,我去掉了诸如登陆,个人中心之类的页面,假设你是一个已登录用户,然后我们来看看如何编写测试。 前端开发 根据通常的做法,前后端分离之后,我们很容易mock一些数据来自己测试: [ { "id": 1, "url": "http://abruzzi.github.com/2015/03/list-comprehension-in-python/", "title": "Python中的 list comprehension 以及 generator", "publicDate": "2015年3月20日" }, { "id": 2, "url": "http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/", "title": "使用inotify/fswatch构建自动监控脚本", "publicDate": "2015年2月1日" }, { "id": 3, "url": "http://abruzzi....

June 22, 2015 2 min

使用Karma作为JavaScript的测试Runner

Karma 简介 Karma是一个JavaScript的测试运行器。事实上,Karma更是一个测试环境,使用Karma可以很方便的的运行测试(方便到你感觉不到它的实际存在)。 一般的TDD的开发流程为: 编写测试(一个会失败的case) 运行测试,并看到这个测试失败 编写代码(足够让测试通过的代码) 运行测试,并看到测试通过 重构 运行测试,并看到测试通过 然后如此循环,而在前端开发中,很长一段时间,这个流程受限于开发环境,比如添加了一个新的JavaScript源文件,开发者需要在HTML中引入相应地文件,以及响应的测试文件,然后刷新页面(有时候还需要清空浏览器缓存)。 在这个过程中,开发者真正关注的就是编写测试,运行测试,编写实现,重构等等,需要不断的重复这个过程。而不是关注如刷新页面,清空缓存,修改HTML对脚本的引用等武馆的工作。 Karma就是这样一个开发环境,开发者指定需要测试的脚本/测试文件,需要运行的浏览器等信息,Karma会在后台自动监控文件的修改,并启动一个浏览器与Karma的服务器连接,这样当源代码或者测试发生修改后,Karma会自动运行测试。 开发者可以指定不同的浏览器,甚至可以跨设备。由于Karma只是一个运行器,你可以使用项目中正在使用的测试框架如Jasmine,QUnit等,甚至可以自定义适配器来支持你自己的测试框架。 运行Karma Karma需要一个配置文件来知道哪些文件需要被加载,需要被监控(当文件内容发生变化时,尝试运行测试),这个配置文件可以通过Karma自带的参数来生成。 基本使用 Karma被实现为一个npm的包,所以可以通过 $ npm install -g karma 安装之后,可以生成karma需要的配置文件: $ karma init my.conf.js karma会让你回答一些问题,比如是哪种测试框架,哪些文件需要被测试,哪些浏览器需要被考虑等。生成的配置文件的一个片段是: // base path, that will be used to resolve files and exclude basePath = ''; // list of files / patterns to load in the browser files = [ JASMINE, JASMINE_ADAPTER, 'src/**/*.js', 'test/**/*spec.js' ]; // web server port port = 9876; // browsers browsers = ['Chrome']; 更新 新的配置文件生成脚本会生成更加模块化的配置:...

October 8, 2013 1 min