如何做实时监控?—— 参考 Spring Boot 实现

December 9th, 2014 1 comment

随着微服务的流行,相比较以前一个大型应用程序搞定所有需求,我们现在更倾向于把大型应用程序切分成多个微服务,服务之间通过 RPC 调用。微服务架构的好处非常多,例如稳定的服务变化较少,不会被非稳定服务所影响;不同的服务更方便交给不同的人管理;发布、扩容等操作也更加有针对性。不过这也不是没有代价的,额外的成本最主要的可能就是运维成本。

我们维护的一个产品,由 7 个微服务构成,它们各司其职,承担上行、下行、同步等各类职责,我非常喜欢这种架构,但也面临一个小小的烦恼。每次我们发布其中一个或者多个服务,就需要去验证服务的健康度,极限情况下,7 个服务 x (国内环境 + 国外环境)x (预发布环境 + 生产环境),总共需要验证 28 次!我希望有简单、标准、自动的方式去验证这些服务是否健康。当然,验证健康也不是跑一个完整的回归测试,那是在测试环境就需要完成的事情,健康检查基本只是关注环境是否 OK,最核心的一两个用例是否 OK。由于部署到预发布或者线上的代码,和线下测试的代码是一致的,因此就不需要重复验证各种功能了,关注点应该在环境上,这一点线上和线下是有明显区别的。至于环境区别,通常就是磁盘、数据库、其他分布式服务等等。

此外,我还希望所有服务的健康检查接口是完全一致的,没有人希望检查服务 A 的时候用 url /ok,检查服务 B 的时候用 url /good。

我曾尝试定义一个健康检查协议,让所有服务都暴露一个HTTP接口 http://172.20.10.2/health.json,返回的内容就包含这个这个服务的基本状态。

这几天看 Spring Boot ,发现它已经很好地集成了我想要的功能,而且看起来更简单,因此我就直接扔掉了自己定义的协议,改而使用 Spring Boot 的方式,Spring Boot 有一个称之为 endpoint 的概念,每个 endpoint 是一个非常简单的 HTTP 接口,用户可以通过 endpoint 监控 Spring Boot 应用,甚至与之交互。这其中,最简单的 endpoint 就是 health,只要加入必要的 Spring Boot 依赖,用户就能通过 health 查看 Spring Boot 应用的基本状态。

这里我们看到服务的状态是 UP,不过也许这个检查太简单了,例如我的服务依赖其他外部服务,其中一个 Tair,一个是 TFS,这两个都是强依赖,如果它们有问题,我的服务就应该是 DOWN 的状态,在 Spring Boot 中,可以这么扩展:

只要加入一个 bean 实现 HealthIndicator 就能实现更加全面的检查,现在访问 health endpoint 是这样的:

只要在每个服务稍微实现一些基本的环境检查,那我就可以用几行脚本快速地完成 7 个服务 x (国内环境 + 国外环境)x (预发布环境 + 生产环境)的健康检查,如果有哪个服务出问题了,定位环境问题也是非常方便的。

这种监控是实时的,这一点非常重要。在实际工作中我们其实有非常完善的系统监控平台,平台能提供 CPU、内存、磁盘、网络IO、JVM 等等各种各样非常全面的信息,这种平台的优势有历史趋势记录,有汇总,有比较,劣势就是不够实时,通常只能看到 5 分钟前的数据。因此,在发布服务,扩容的时候,等待这样的系统监控平台反馈就不够了。

除了 health endpoint 之外,Spring Boot 还提供了其它10多个 endpoint,它们都是针对运维设计的,例如可以用 shutdown endpoint 来关闭服务、用 beans endpoint 来查看所有的 Spring Bean,下面我想详细讲一下 metrics 这个 endpoint。

默认访问 metrics 我们能得到很多信息,包括 JVM 的线程数、内存、GC 数据等等……,这些都是系统级别的数据,但其实我们可以通过 metrics 收集实时的业务数据,例如每分钟用户登陆数量、每分钟文件同步数量、实时的缓存命中率……等等。

实现是这样的:

Spring Boot 内置了两个 Service,CounterService 可以用来做简单的累加累减,GaugeService 可以用来存放简单的 double 值,数据都存放在内存中。

现在访问 metrics endpoint 的效果是这样的:

Spring Boot 的 metrics endpoint 带了很多的信息,这里我们只关注自定义的数据。

如果所有服务的核心业务数据都通过 metrics 暴露,我们接下来要做的无非就是通过一些数据可视化的 JavaScript 组件访问这些数据,做成一个 Dashboard,那我们就能通过这样一个 Dashboard 查看系统的实时状态。

Spring Boot 的 Endpoints 带着强烈的 DevOps 色彩,“you build it, you run it”,开发不仅要关心如何实现功能,还需要关心服务在线上运行的状态,如果缺乏实时监控,维护线上服务必然是一场噩梦。如果基于 Spring Boot 开发服务,那只需要稍作扩展,实时监控就足够用了,就算不使用 Spring Boot,类似的思路自己实现也并不复杂。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/12/09/real-time-monitoring-with-spring-boot/

Categories: DevOps Tags: , , ,

预防错误的设计

October 28th, 2014 1 comment

上周参加了一个 Michael Feathers 的 workshop,整个 workshop 内容非常多,其中大部分是围绕他那本著名的《修改代码的艺术》所阐述,不过除此之外 Michael 还以 Beyond Error Handling, Using Design to Prevent Errors 为主题,讲了不少如何优雅地处理错误的做法和思路。其中的一个例子涉及并融合了面向对象和函数式的相关知识,引发我的一些思考,本文就此做一些总结。

例子的代码很典型也很简单,简化后,主要涉及三个类,分别是 Inventory, Item, 和 Sale,代码如下:

想象我们去超市购物,然后到收银台结账,当我们把一车东西交给收银员的时候,她会对每件东西(Item)逐一扫描条形码(barcode),扫描条形码的时候系统会去库存(Inventory)中查询这件东西的名称和价格,扫描完成后,收银员会计算总价格(subtotal)。上述代码所表述的就是这个场景。

读者不妨花5分钟理解一下上述代码,接着考虑一个问题,如果扫描的条形码在库存中不存在怎么办?具体来说 inventory.itemForBarcode(barcode); 返回 null 怎么办?下面的所有讨论都围绕这个问题展开。

null 判断

如果我们随意把 null 传来传去,什么都不干,那必然早晚会出现 NullPointerException ,那么最简单的做法是加上 null 判断以保证错误的 null 不会再被传出去:

这可能是我们能见到的最常见的做法,代码中到处充满了 null 判断,在某种程度上把代码主要想表达的主干逻辑给污染了,过多 null 判断影响了代码的可读性。如果所有使用变量的地方都要去检查 null,那代码必然会恶心的不行,实际中我们不会这么干,因为变量的值什么来的,很多时候也是由我们自身控制的。但如果我们在使用第三方的代码,那为了保护自己,检查 null 则是比较明智的选择了。因此,在自己的代码库内部,尽量避免传 null 并去掉 null 判断;而在集成第三方代码的时候,则多防御性地使用 null 判断

如果我们设计一个 API,那显式地告诉别人是否可以传入 null,是否会返回 null,则是很好的习惯,例如:

上述代码表示传入的 barcode 不能为 null(如果是 null,不保证会发生什么后果),而返回的 Item 则可能是 null,这样实现的时候我就不需要去判断 barcode 是否为 null,另外用这个接口的人也明确知道自己要检查返回是否为 null。@Nullable 和 @Notnull 都已经在 jsr-305 中明确定义。

异常

避免 null 判断的一种最直接做法就是抛出异常,例如:

遇到 ItemNotFoundException 怎么办?继续往上抛?处理掉?记个日志然后吃掉?在这个上下文中,我无法给出明确的答案,具体选择哪种方式还得看具体场景,通常来说,**先想想当你遇个异常的时候,你的用户希望得到怎样的结果?然后基于这点来指导你的异常处理方式。**

异常在很多情况下是很适用的,例如磁盘坏了,那抛出 IOException 让系统 Fail Fast 往往是一种很好的处理方式,然后异常也有不少问题,首先 Java 中的 Checked Exception 很容易让 API 变得丑陋无比;其次异常的跳跃式的抛来抛去,也让有 goto 的感觉,很难 follow 。

有没有更好的办法保持 API 的简洁,同时也能避免 null 判断呢?

Null Object 模式

在面向对象设计中,我们常常可以使用 Null Object Pattern 来去掉 null 检查逻辑或者异常捕捉,该例中,我们可以创建一个 NotFoundItem ,它继承自 Item,不过 name 和 price 比较特殊:

然后再 Invetory 中适当地返回 NotFoundItem:

这样,所有使用 Inventory.itemForBarcode() 的地方都不需要特殊的错误处理了,例如在 Sale 类中,addBarcode() 和 subtotal() 都能正常工作。想象一下,如果有五个、十个、或者更多的类使用 Inventory.itemForBarcode(),这样可以简化多少代码!因此,如果有可能,尽量在下层完成错误处理,因为越往上层,需要的错误处理代码往往越多。这实际上是契合 Robustness Principle 的,这条原则是这么说的:

Code that sends commands or data to other machines (or to other programs on the same machine) should conform completely to the specifications, but code that receives input should accept non-conformant input as long as the meaning is clear.

Inventory.itemForBarcode() 能够接受不合法的 barcode ,但是它返回的 Item 是符合接口定义的,没有特殊性。

到目前为止一切似乎看起来很美好,但实际上 Null Object 模式不是完美的,想象一下,如果我们要在 Sale 类中加入这样一个逻辑:如果购买物品的数量达到了10,则有5%的折扣。显然 NotFoundItem 会破坏这样的逻辑,扫描1个合法 barcode 加9个不合法的 barcode 也会造成 5% 折扣。

Option

我们花了大量的精力对付 null,事实上 null 这个概念的发明人 Tony Hoare 也说:

I call it my billion-dollar mistake.

是否有其他更好的方案来解决这个问题呢?让我们来到函数式的世界,看看 Scala 是怎么对付 null 的。Scala 内置了一个特殊类型,叫做 Option,顾名思义,可以认为 Option 可能包含一个值,也可能不包含,例如我们可以修改 Inventory.itemForBarcode() 如下:

这一段 Scala 代码也比较好理解,*itemForBarcode* 接收一个 String 参数,返回 Option[Item],而*Option[Item]* 有两种子类型,一种是 Some[Item],表示有实际的 Item,另外一种是 None,表示没有。

现在 Sale.addBarcode() 是这么处理 Option[Item] 的:

代码中对 Option[Item] 进行了迭代访问,与迭代一个集合的做法完全一样,这么做的优雅之处在于,如果 Option[Item]是 None,迭代中的逻辑不会被执行,也不会有任何副作用,与我们迭代一个空的集合一样。当然,如果 Option[Item]是 Some[Item],则 item 会被取出来并执行相应的逻辑。因此我们可以简化地把 Option 认为是一个包含1个或者0个元素的集合类。

事实上 Scala 的 Library 中大量使用了 Option,例如,由于 Scala 中的 Map 实际上有方法返回 Option,因此 Inventory.itemForBarcode() 可以简化成:

现在,*Inventory.itemForBarcode()* 接口的协议是:我会返回一个 Option[Item],您自己去迭代里面的内容。有没有可能把接口简化下?让用户(这里的 Sale )不必关心迭代呢?让接口的协议变成:我会去找 Item,找到的话帮你执行逻辑 blabla ……

高阶函数( Lambda 表达式)

要回答上面的问题,我们得看一下另一个 Scala 的函数式特性,那就是高阶函数,如果是 Java 8 ,那就是 Lambda 表达式,我们可以这样定义 Inventory.itemForBarcode(),它接收一个 barcode 和一个接收 Item 的函数 f :

如果能找到 item ,f 就会执行,现在 Sale.addBarcode() 变成了:

面向对象设计中有一条著名的原则:Tell, Don’t Ask,《Smalltalk by Example》一书的作者这样描述该法则:

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.

从 Inventory 获取数据( item 也好,null 也好,Option[Item] 也好),然后根据其内容是否存在再做操作,更多是过程式思想;相对的,扔给 Inventory 一个 barcode 和一段函数则是所谓的 Tell :去查一查,查到就干这个!

上述的代码用 Java 8 Lambda 表达式也能轻松实现,但如果是 Java 6/7 ,就比较麻烦了,你得搞一个 interface,然后传一个匿名内部类,非常麻烦。

小结

整个过程我接触了 null, 异常, Null Object Pattern, Option, 高阶函数等概念,这里有过程式编程范式、面向对象编程范式、函数式编程范式,这是最让我惊异的,各种编程范式对于错误处理有着不同的方式,你会发现 Java 作为一门面向对象语言其实糅合了不少过程式的处理方式,而 Scala 或者 Java 8 则有函数式的处理方式,而且总体上来说函数式的处理方式更加优雅。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/10/28/using-design-to-prevent-errors/

Scala元编程之Dynamic

August 31st, 2014 No comments

scala_logo

有一点 Ruby 经验的朋友都知道 Ruby 的元编程威力十分强大,Rails 中的 ActiveRecord 就是一个极佳的例子,Metaprogramming Ruby 对 Ruby 对元编程原理有着非常深入的阐述。此外,七周七语言 中也有一段介绍 Ruby 元编程的实例代码,主要是用到了 method_missing:

这段代码简单实现了一道非常著名的 Coding Kata 练习题,要实现的逻辑是把 I, II, IX 之类的罗马数字转换成阿拉伯数字,规则可参考 维基百科 解释,不过这不是本文的重点,这段代码中最有意思的是这么一行:

Ruby 支持定义一个 method_missing 方法,它带有一个名字和多个可选参数,当 RVM 执行 Ruby 代码发现类 Roman 不存在 XC 方法的时候,就会去看是否有 method_missing 实现,这该例中实现了,就继续执行下去,相当于 Roman.XC 是动态生成的一个方法,可以近似地说,我们写的代码会在运行时生成代码并执行,这就是元编程。就像生成罗马数字的阿拉伯数字一样,传统的编程方式,要实现的方法数量几乎是无限的,而元编程能让我们动态的生成方法,适当使用,非常优雅。

Ruby 是动态类型的语言,因此能做到上述功能一点不奇怪,而 Scala 是静态类型的,要让他动态地生成方法似乎不太容易,不过从 Scala 2.10 引入的 Dynamic 毫无困难地做到了与 method_missing 几乎一致的功能,下面是 Roman 的 Scala 版:

上述代码通过 import scala.language.dynamics 开启 Dynamic 功能,然后让需要实现动态方法的 Roman 继承 Dynamic trait 以获得 Dynamic 特性,最关键的是这一行:

它接受一个 String 参数并返回 Int,这正是调用的时候代码 Roman.VI 所期望的,对比 Ruby 一个 method_missing 方法,Scala 共提供了四个方法:

- applyDynamic
- applyDynamicNamed
- selectDynamic
- updateDynamic

它们各自对应的用法可以参考 Scala 2.10: class OhMy extends Dynamic !

Scala Dynamic 和 Ruby method_missing 看起来差不多,不同的是 Ruby 少了一点点代码,而 Scala 多了些编译器检查。对于喜爱 Scala 或者在工作中需要编写 Scala 的码农来说,多了一个本来只有在 Ruby 那里才听到的特性,那是再好的消息不过了。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/08/31/scala-metaprogramming-dynamic/

Categories: 编程语言 Tags: , ,

dig与dns基本理论——解析和缓存

August 4th, 2014 2 comments

DNS(Domain Name System,域名系统)也许是我们在网络中最常用到的服务,它把容易记住的域名,如 www.google.com 翻译成人类不易记住的IP地址,如 173.194.127.132,以便在方便人类输入的基础上让计算机能够精确地在网络中找到目标机器。dig(Domain Information Groper)是一个比较常用的 dns 分析及调试工具,一般 Linux 和 Mac 都已默认安装。本文将基于 dig 的使用解释 dns 的基本知识。

解析

如果我们想知道 www.juvenxu.com 被解析到什么 IP 地址,可以用如下命令(我知道可以用ping,但本文主要围绕 dig 讲述):

命令行参数 +noall 和 +answer 只是为了输出更精简。

暂时忽略第3、4行,着重看最后一行,它分为5部分,第1部分是我们查询的域名;第2部分是缓存失效时间ttl(秒),暂且不关心;第3部分是 IN,表示类别 Internet,一般也都是这个类别;第4部分很重要,表示这行记录的种类(type),该行种类是 A(Address),说明这是一个A记录,描述域名对应的地址;第5部分好理解,就是地址了。

我们输入了 dig 命令和想查询的域名,得到了结果 IP 地址,本地的 dig 命令自然不可能知道世界上所有域名对应的 IP 地址(或者说A记录),那是谁告诉它的呢?

加上 +stats 让 dig 打印基本的统计信息,其中第7行告诉我们 SERVER 是 192.168.1.1,53是DNS使用的 UDP(有时候可以是TCP)端口。dig 默认使用 /etc/resolv.conf 配置的域名服务器:

我们可以指定 dig 使用其他域名服务器帮我们解析域名,例如用 8.8.8.8:

这条命令中的 @8.8.8.8 表示使用 8.8.8.8 解析域名。

有些互联网服务部署在全球各个地方,这个时候服务提供方就希望中国的用户访问中国机房IP,美国的用户访问美国机房IP,这个时候两个地方的域名服务器返回的结果就会不一样,我们可以用类似上述的命令验证这一点。读者可以参考国内外DNS服务器地址列表,用各个国家的服务器 dig 一下知名站点如 google.com 试试。

不过即便如此,192.168.1.1 也好,8.8.8.8 也好,都不可能知道全球所有的域名信息,事实上,全世界数以亿次的域名信息分布在无数的域名服务器上,我们可以看一下维护 www.juvenxu.com 域名信息的服务器是什么:

我们在 dig www.juvenxu.com 后面加了 ns 参数,表示希望得到域名服务器信息(而不是默认的A记录),不过这次查询我们没有得到任何结果。没有关系,这往往表示该域名的上一级域名(juvenxu.com)的域名服务器管理这个子域名,因此我们 dig juvenxu.com:

结果有两行记录,和 dig A记录结果一样,各自分成5部分,第1,2,3部分和A记录结果含义一致,第4部分表示表示种类(type)是 NS(Name Server,域名服务器),第5部分自然就是域名服务器的地址了。

现在我们知道 dig 请求域名服务器 192.168.1.1 juvenxu.com 的域名信息,而 ns13.ixwebhosting.com 和 ns14.ixwebhosting.com 保存了 juvenxu.com 的域名信息,那么 192.168.1.1 怎么知道如何联系上 ns13.ixwebhosting.com 或 ns14.ixwebhosting.com 呢?这就涉及到 DNS 的分布式架构了。

整个 DNS 服务是按照树形的结构分布的,最顶级是根域名,根域名服务器保存了所有定级域名服务器的信息,顶级域名包括 com, net, edu, org, cn 等等,各个定级域名服务器又包含了下一级的域名服务器信息,如 com 域名服务器包含了 microsoft.com, amazon.com 这些域的服务器信息,这样一层层往下:

dns_structure

我们可以使用 dig 的 +trace 选项来仔细查看这个过程:

dig 直接请求的 192.168.1.1 并不知道 juvenxu.com 的 A记录是什么,它就去让 dig 去问根域名服务器,根域名服务器也不知道,让 dig 去问 com 的域名服务器,com 域名服务器也不知道,就让 dig 去问 ns13.ixwebhosting.com 和 ns14.ixwebhosting.com,而我们知道,这两个域名服务器保存了 juvenxu.com 的 A记录。这种一问一答的解析方式,叫做迭代解析(Iterative resolution)。这种方式下,每个域名服务器只是给出它所知道的最佳答案。

另外一种解析方式是递归解析(recursive resolution),如果我们不给 dig 加上 +trace 参数,dig 就会给域名服务器 192.168.1.1 一个递归解析请求,等于告诉是让 192.168.1.1 直接负责去解析到最终答案,然后直接返回给 dig。通常情况下客户端(dig)对域名服务器发起的递归解析请求,而域名服务器对其他域名服务器的请求(如 192.168.1.1 对根域名服务器)是迭代解析请求。

缓存

如果世界上所有网络设备(PC,服务器,手机,平板电脑……)的每次网络请求都要访问 DNS 根服务器,那这个量就实在是太大了,根服务器肯定受不了。考虑到一个域名和其IP的对应关系其实不会太常变化,因此在各个域名服务器缓存之前请求过的域名A记录就是个不错的策略。基于此,对于一个域名如 douban.com 来说,域名服务器分成两类,一类是切切实实保存该域名信息的服务器,称为权威域名服务器(Authoritative Name Server),其余的仅仅是缓存其域名信息的服务器,就是缓存域名服务器。

例如,如果我很久以来第一次 dig douban.com,就会得到类似这样的输出:

这段输出比较长,我们重点看15-17行,即 AUTHORITY SECTION 部分,由于我们长久以来第一次 dig douban.com,因此就近的域名服务器还没有 192.168.1.1 还没有缓存 douban.com 的 A记录,因此它会解析到 douban.com 的权威域名服务器,再返回给我们结果,这个结果是最新的,是“权威”的。

我们回头在看13行A记录的第二部分 ttl,即缓存失效时间,值是35,表示该缓存35秒后失效,换言之,35秒内我们再请求 douban.com 的话,192.168.1.1 会直接返回缓存的A记录:

这时就没有 AUTHORITY SECTION 了,说明结果来自缓存域名服务器,而不是权威域名服务器。

如果权威域名服务器改变了A记录信息,而缓存服务器还没有过期,我们怎么去主动了解最新的A记录呢?或者说,怎么主动去权威服务器查找A记录信息呢?简单,解析的时候指定域名服务器即可:

缓存机制能够减轻域名服务器尤其是根域名服务器和定级域名服务器的压力,让域名解析过程更快,当然和所有缓存一样,副作用也是有的,当权威服务器修改域名信息的时候,缓存服务器的信息就过时了,要等TTL失效后才能解析到最新正确的信息。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/08/04/dig-and-basic-dns-resolving-and-cache/

Categories: DevOps Tags: ,

基于Cucumber和Scala的验收测试

July 8th, 2014 No comments

Cucumber是一款富有盛名的 BDD(行为驱动开发)框架,特别是在 Ruby 社区,它有着很高的知名度,然而大家可能不了解的是,Cucumber 通过 cucumber-jvm 很好地支持了 jvm 平台上的各类语言,包括 Java、Clojure、Scala、Groovy 等等。我工作中的产品代码是 Java,直接用 Ruby 测试 Java 应用程序有很多不便(比如用调用 Java 客户端,使用 Java 工具类库),而用 Java/Scala 等语言测试就比较方便,实际中我们使用的是 Scala,主要是它更简洁,这也是一个我们锻炼学习新兴语言的机会。本文介绍如何搭建一套基于 Cucumber 和 Scala 的测试环境。

首先,基本的 POM 依赖如下(这里使用 Maven,如果你用 Gradle 或者 sbt,只能自行搜索方案了):

上述三个依赖分别是 cucumber-jvm 的核心类库、scala支持库、以及junit集成库,集成junit主要是方便IDE和Maven通过JUnit运行Cucumber。除此之外,你当然还需要Scala依赖,包括 scala-compiler、scala-library和我们后面会用到的 scalatest:

为了让Maven能够编译Scala,还需要配置 maven-scala-plugin:

基本的项目配置就绪,接下来编写我的 cucumber feature 文件,sample.feature,该文件位于项目 src/test/resources/ 下的 features/ 子目录下,为了方便组织 feature 文件,我一般把所有 feature 文件都放在这里,由于如何写 feature 不是本文重点,例子就很简单了:

该 feature 对应了两个 StepDef,我用 Scala 编写,位于 src/test/scala/目录,具体类为 com.juvenxu.cucumber.scala.steps.SampleStepDefs

* 该例中 ScalaDsl 定义了 Given, When, Then 这些 DSL,When 和 Then 的参数是一个用来匹配 Step 的正则表达式,Scala 的三引号用起来更方便,再也不用给 String 中的引号转义了。
* trait EN 定义了 DSL 的英文表达,如果你想写“假设”、“当”、“那么”等中文,可以用 trait ZH_CN。
* ShouldMatchers 是 scalatest 的东西,代码中的 result should be(arg0) 就是基于 scalatest 的断言,对比 Java 的测试框架,表述力更强大。

最后我们还需要一点简单的代码来驱动 Cucumber,它也用 Scala 编写,位于 src/test/scala/目录,具体类为 com.juvenxu.cucumber.scala.RunCukesTest

* @RunWith 就是告诉 JUnit 用我的 Cucumber 这个 Runner 来跑测试。
* @Cucumber.Options 中的 format 指定生成的测试报告,后续可以 target/ 目录下找到。
* @Cucumber.Options 中的 features 指定 features 文件的路径,该例中是 classpath 下的 features 目录下。

至此,整个项目的目录结构是这样的:

在命令行运行 mvn clean install 能看到如下输出:

关于和 IDE 的集成 IntelliJ IDEA 做得很棒,只需要安装一些插件就可以了,需要安装的插件包括:Scala,Cucumber for Java,以及 Cucumber for Scala,效果如图:

cukes_features

cukes_feature_sample

实际工作中使用 Cucumber 的体会,主要还是它会迫使我们把测试写清楚,用自然语言地方式描述测试,就必然要求你把测试想得比较清楚,对比我们在其他代码库中随处可见的各种莫名其妙的 TestCase,Cucumber 测试的可维护性高很多,通过测试就能快速了解一个系统的各类功能。技术上来说,它并不比纯的 JUnit TestCase 高明多少。

最后,我和江平兄一起翻译的《Cucumber:行为驱动开发指南》是本不错的学习 Cucumber 的参考书籍,虽然书中的代码都是 Ruby 的,但思路完全是一致的,特此推荐。

cucumber_book


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/07/08/acceptance-test-with-cucumber-and-scala/

Categories: 测试 Tags: , , , ,

ScrumGathering上海2014

June 8th, 2014 4 comments

6月5-7日是一年一度的、以敏捷为主题的ScrumGathering大会,今年我作为讲师身份参加,除了能够免费参会这一好处之外,还可以总结下自己最近一年的思考,另外还能见到不少许久未见的朋友。然而令人意外的是,这次参会得以有机会和自己一直很崇敬的Dave Thomas详谈,这远远超出了我对大会的期望。

如果你参加过以敏捷为主题的大会,你就会发现很多热度很高的词:Agile、转型、Scrum、CSM、Lean、TDD…… 这些名字都是领域的专家发明的,专家们信誓旦旦地告诉你,他们能解决你面临的问题。但是不知道大家想过没有,在软件开发这个行业,解决问题最终基本都会落实到写代码上,然后在大会上,能看到代码的地方聊聊无几,这是不是有点问题?

第一眼看到 Dave 的时候,他腿上放着个Macbook Air在休息区和朋友申健在聊天,一开始没留意,后来有人告诉我这是 Dave,我略吃惊,忙求引荐。Say Hi 之后立刻说我看过 The Pragmatic Programmer,喜欢 Pragmatic BookShelf …… 一堆真心的“奉承”话,我的确曾经有一段时间把 The Pragmatic Programmer 一书放床头,每晚翻翻,细细体会,这书也是我看过的最好的IT书籍之一。客气完毕之后,我问了他对 Reactive思想和 Akka 的看法,Dave肯定了我的想法,认为这是编程模型发展的方向(虽然他也说了,这玩意儿几十年前就有了),然后他就开始high了,开始给我推荐 Elixir,我说这语言我没听说过啊,哦,记得好像有本书,当时以为是一个什么类库呢。Dave于是就打开Mac给我写Elixir代码,尼玛,写代码!在一个以敏捷为主题的大会上,一墙之隔的会场内,有讲师在讲酷炫的名词呢。Dave顺手给我写一段Elixir代码,类似于Java 8 Stream API的东西,基于消息传递,看起来非常简洁,很像Bash。我们还聊了Pragmatic Bookshelf,说起《七周七语言》,说起《番茄工作法图解》,Dave还透露了他们计划出版的《more seven languages in seven weeks》,里面会介绍 lua,elixir,和其他另外5种我都没听说过的语言。

Juven_and_dave

大会上我的话题是”务实地拥抱极限编程”,大意摘要如下:

1. 极限编程作为一个比较流行的敏捷方法,大家都比较熟悉了,尤其是TDD、持续集成、结对编程、重构能具体实践。
2. 但是大家是否忘记了极限编程的价值(Values)?
3. 极限编程推崇的价值是 Feedback, Communication, Simplicity, Courage, Respect
4. 极限编程实践都有其对应的价值,例如TDD就对应了Feedback
5. 我们不应该只盯着实践,我们应该思考价值,并总结能实实在在解决自己问题的实践
6. 演讲用实际的例子介绍我们怎么遵循价值、总结实践处理具体问题。


(slideshare被墙,点此下载pdf)

敏捷要解决实际问题,现有实践能解决问题那当然就直接用,现有实践搞不定的时候,我们需要寻找其他办法,寻找其他办法的过程中,也要有指导,解决完问题了,也得有总结,这个时候,极限编程的Values就是很高的指导及方向。

有幸我把自己的topic提前和Dave分享并得到了他的肯定,并且后续他也走进会场听了我的中文演讲,据我观察这是他在整个大会中听的唯一的一场演讲,这给我受宠若惊的感觉。Dave的演讲是《敏捷已死,但敏捷精神长存》,核心思想还是围绕了Pragmatic这个词,他的意思说,大家别听一像火鸡一般自以为是的专家兜售玄乎的方法,大家要自己实践、自己思考、自己总结,这才是敏捷精神,如果大家只做专家们所兜售的敏捷,那敏捷就死了。另外他介绍自己的时候,说自己是个Programmer,而且到现在还是每天在写代码。Dave是敏捷宣言的作者之一,反观我们周围,那么多说敏捷的人,多久没写过一行代码了?

我认真反思了一下自己,很多时候我宁愿看书而不愿拿电脑敲一个DEMO,觉得弄明白理论后,代码自然写起来很简单,然后实际会发现写代码很慢,很吃力,看似简单的理论,看似理解了,写代码的时候还是磕磕绊绊。另外,随着工作年限的增加,很多时候解决问题更多是协调沟通而不是写代码,这样练习的机会就渐渐变少,看来需要刻意弥补。

有时候我会问自己,为什么做程序员,很大一个原因就是一开始编程所感受到那种神奇,而现在,有多久没那种感受了?生活虽然不易,还是不能忘了初心。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/06/08/scrumgathering-shanghai-2014/

Categories: 敏捷 Tags: , ,