• 2014-07-02

    Moco 0.9.2发布

    Tag:moco

    前版信息:Moco 0.9.1发布

    我很高兴地宣布,Moco 0.9.2发布了。

    Moco是什么?

    Moco是一个可以轻松搭建测试服务器的框架/工具/程序库。

    变更

    本次发布最大的变更是加入了HTTPS的支持。

    HTTPS服务器的创建即不同于普通的HTTP服务器,它使用的是httpsServer方法,除了类似于HTTP服务器的参数之外,一个很重要的参数是certificate,这里需要给出相应文件以及对应keystore密码和certificate密码。

    final HttpsCertificate certificate = certificate(pathResource("cert.jks"), "mocohttps", "mocohttps");
    final HttpsServer server = httpsServer(12306, certificate);

    独立服务器用户也可以通过命令行生成一个HTTPS服务器:

    java -jar moco-runner-<version>-standalone.jar start -p 12306 -c foo.json --https /path/to/cert.jks --cert mocohttps --keystore mocohttps

    还有一个比较重要的调整,在JSON配置中,增加了直接对JSON的支持,比如,

    {
       "request": {
           "uri": "/json_response_shortcut"
       },
       "response": {
           "json": {
               "foo" : "bar"
           }
       }
    }

    这样,给出应答就会是一个JSON对象:

    {
       "foo" : "bar"
    }

    而原来的做法如果需要返回一个JSON对象,需要大量的转义字符或是存放到文件中。

    在API方面,也做了许多调整:

    • 在Java API中,增加了HTTP版本协议类,无需以字符串的方式制定HTTP版本。
    • 在匹配方面,增加了更多的运算符,比如startsWith、endsWith、contain、exist。
    • 对于RequestHit的验证,增加了between运算符,可以判断请求次数在某个区间内。
    • 增加了多个Request Monitor的接口,以便处理遗留代码的时候,可以同时进行验证和查看日志。
    • 在模板接口上,将模板变量的类型由Object接口改成了String,这样,API用户必须确定好模板变量具体的表现形式。

    更多的细节请参考ReleaseNotes

    感谢

    感谢Michal Svab,实现了HTTPS API部分。

  • 在单元测试中,与时间相关的测试总是让人很头疼。举个例子,我们希望做一个定期过期缓存,比如30分钟过期,这该怎么测试呢?等30分钟?那要是过期时间是3天,你打算把开发时间全部交给等待,然后向老板汇报,我在等测试。祝你有一个糊涂的老板!

    我们不妨分析一下,看看有没有什么不那么令人发指的解决方案。以上面的缓存为例,缓存怎么知道过了多长时间,它肯定是在哪取个时间?对于Java应用来说,很有可能最终就是调到System.currentTimeMillis()或者nanoTime()。但是,这两个方法都是静态方法,想用Mock的人可以休息了。

    那么直接调用操作系统的方式修改时间呢?可以,只是一旦与具体的OS相关,各种跨平台的问题就随之而来,总而言之,麻烦。

    许多计算机问题都是可以通过引入间接层来搞定的,这一次又到了“间接层”的表演时间了。正如前面所说,与时间相关的应用最终可能都要到最底层那两个方法。如果我们可以做一层隔离,让所有的调用都来到间接层,我们就可以在间接层上做手脚了。

    事实上,这是一个如此通用的问题,以致于我们甚至都不需要提供自己的解决方案。

    在Guava中,有一个类叫Ticker,它提供了一个方法叫做read(),他就是我们的间接层。缺省情况下,我们会直接使用Ticker.systemTicker(),顾名思义,它是系统提供的ticker,read方法最终会调用倒System.nanoTime()。如果测试需要,我们可以自己Mock出来一个Ticker,就像下面这样:

    Ticker ticker = mock(Ticker.class);
    long time = Ticker.systemTicker().read();
    when(ticker.read()).thenReturn(time, time + TimeUnit.SECONDS.toNanos(10));

    这段代码第一次调用时返回一个时间,第二次调用返回的时间就是第一次调用的10秒之后了。所以,如果你的代码依赖于Ticker,剩下的魔法你都可以自己完成了。

    不过,这还不是终点。

    在Java中,谈及时间和日期,Joda Time已经是一个必选项了。实际上,Joda Time也为自己的用户提供了一套解决时间测试的方案,它有一个DateTimeUtils.setCurrentMillisFixed方法,我们可以传入一个固定的时间,比如,10秒后。时间就固定在那了。当然,如果你需要让时间重新流动起来,需要调用一下DateTimeUtils.setCurrentMillisSystem。

    原理很简单,Joda Time底层最终会调用DateTimeUtils.currentTimeMillis方法,而我们调用的set方法就会让这个方法返回不同的值,当然,缺省的是系统时间。

    Ticker和DateTimeUtils.currentTimeMillis在单位上有个差别,Ticker用的是System.nanoTime(),而DateTimeUtils.currentTimeMillis则如名所示,使用的是System.currentTimeMillis()。

    好了,这就是测试“时间”的方法。

  • 数据,是软件处理的核心,我们写各种各样的应用都是为了处理数据。处理数据有一个非常重要的前提,数据是合法的。一个计算数字的应用如何面对一堆字符呢?为了保证应用可以正常运行,各种校验是必不可少的。

    问题来了,校验该在哪做呢?

    显然到处做校验不是一个好主意,那到底服务层、数据层,还是接口层,才是校验的藏身之所呢?

    为了不让校验四处进行,一个建议的做法是在数据的入口进行校验,保证所有进入系统的数据都是合法的,这样一来,所有逻辑处理的代码只要关心逻辑就好了,而不需要关心数据合法性。比如说,客户端请求就在请求到达的入口进行校验,而从数据库读出的内容,就在数据层进行校验,而如果是集成了其它的服务,则要在读回数据的地方立刻进行校验。数据一到系统,先进行校验,如果有错了,就会立即发现,而不是等到数据跑到了程序里面,出错了,我们再去定位数据的来源,这种做法符合我们常说的“Fail Fast”原则。

    有一个有趣的问题,作为一个对细节特别较真的程序员,如果我的数据是字符串,虽然你说你在接口部分校验了数据,万一你遗忘了,流到了我这里,我不放心啊!所以,我可能还要在我这里写一遍校验。

    对于这个问题,我只能说,谁让你把字符串到处传了?

    事实上,在开发初期,很多东西用字符串表示起来很简单,比如语言,比如Tag。这种不精心的实现就会给未来带来很多麻烦。正如前面这个问题,其实大多数时候,你需要的不是一个字符串,而是一个“东西”,它应该被封装起来。所以,与其纠结于这个字符串是否还要校验,请考虑封装。

    另外一个有趣的问题是,如果我这个数据允许为空怎么办?想想都头疼,到处判断一个对象是否为空。

    如果我们能分清能为空的对象和不能为空的对象,那就再好不过了。幸好我们有了Optional,它给我们提供了一个有意义的空。结合前面的内容,在接口部分,如果数据可能为空,我们就用Optional把它包装起来,而普通对象则直接传递。我们就在程序里有了一个约定:

    • Optional的对象可以为空,需要判断的时候,自己处理一下。
    • 普通对象都不能为空,为空就直接空指针异常,因为那是错的。

    关于Optional,我强烈建议每个Java程序员都去了解。在Java 8里,它已经成为了JDK的一部分,如果你的Java版本还早,Guava做了很好的支持。

  • 做服务端软件,几乎无可避免地会遇到集成。那些年受到SOA“熏陶”,对于很多团队来说,标准的做法似乎是这样的:从另外一个地方弄来一个WSDL,然后,用它生成代码。

    是时候编写自己的代码了,你的团队会怎么做?既然有了生成代码,它基本上也算是很好地反映了我们的业务需求,那就直接用这些类作为我们的领域对象,一切轻轻松松。

    下面是我的一个真实经历。

    还有最后一天,我们的项目就要发布了。这时客户一个技术负责人跑了过来,“有一个接口我们不能用了”,那是我们最重要的一个接口,少了它,整个应用几乎就不可用。不幸中的万幸是,有一个功能等价接口可以用。不过,这个新接口是一个完全不同的协议,一个全新的WSDL。

    先别抱怨为什么这么关键的问题在最后一天才发现。我们先想想这个变动会对系统造成怎样的影响。

    如果我之前的开发是依赖于这些生成代码,将其作为我的领域对象,作为一个核心功能,这就意味着我所有的代码几乎都要依赖于这些类。替换它几乎要把所有代码修改一遍。可明天我们就要上线了!

    你明白我的意思了,直接依赖于这些生成代码,几乎就宣判了项目的死缓。

    间接层,是解决许多所有计算机问题的利器。在这里也不例外。我们对于这种问题的解决方案是,编写自己的领域对象,即便它看上去生成对象一模一样。事实上,在这个问题上,阻碍许多人编写自己领域对象的原因多半就是两个类会一模一样。

    在软件设计中,一个重要的原则就是向着稳定的方向依赖。生成的代码是别人的东西,别人的东西稳定性是什么样的,谁知道呢!即便是JDK,大部分类也不是像我们想的那样稳定,比如日期,随着大家对于这些类日益深入,一些问题就会暴露出来。如果是自己团队编写的代码,至少有一个非常重要的优点,我们拥有控制权,我们可以根据业务发展需要自行修改。依赖别人永远不如依赖自己靠谱。

    回到我的经历上。实际上,我那天真正做的修改就是重新写了一下从生成协议代码到自己领域对象适配部分的代码,因为生成代码与其它部分代码没有任何关系。

  • 2014-05-15

    不部不知道

    Tag:部署

    这是一个关于部署的话题,一句话,部署要趁早。

    故事1

    我们的应用有一个静态导出功能,在自己开发环境中,我们做了无数次导出,没有任何问题。但上到真正的环境中,导出功能一下子不起作用了。

    经过紧张的调试,我们把目光聚集到一个配置上,它是一个用来处理导出的URL,这个URL的地址是一个内部地址。在真正的环境里,每一台机器都会有内部IP和外部IP,我们用来登陆进行配置的是一个内部IP,但用浏览器登陆时,我们却用的是外部IP。所以,当试图访问一个内部IP地址时,自然就访问不到了,于是,导出过程就挂掉了。

    改正很简单,只要把配置改成外部IP即可。

    故事2

    做Web应用,开启gzip压缩是不可避免的。我们在自己的Nginx服务上测试好的配置文件,部署到了真正的环境中,gzip死活不起作用了。最初,我以为是自己的配置没有起作用,结果,用内部地址访问以下,一切正常。可为什么用域名访问就访问,难道域名解析还和gzip压缩有关?

    经过半天紧张的调试,我们把焦点定位在了负载均衡器上。请求通过域名在真正环境是要先到达负载均衡器的,然后,由负载均衡器分发到后台的Nginx上。这种访问模式实际上成了反向代理,所以,在Nginx上,要想gzip在这种模式下工作,需要把gzip的代理模式打开,比如:
         gzip_proxied any

    故事3

    如果我们做的是一个老应用的改版,为了搜索引擎,无可避免的一件事是要兼容老应用的一些URL。初想起来,有一个很简单的做法,在Nginx上配置一些URL rewrite规则即可。

      rewrite /old/url /new/url permanent;

    在我们测试环境一切正常,但是,上了真正的环境,这个跳转一下子不起作用了。原来,在真正的环境里,给外部访问所用的端口与内部部署的端口是不一样的,而这个跳转是正常的,只不过,它跳到了内部的端口,而这个端口在外部是不可见的,于是,出了问题。

    给出一个quick and dirty的解决方案很容易,给出一个绝对地址用于跳转即可:

      rewrite /old/url http://outside/new/url permanent;

    上面的几个小故事遇到的问题其实是一样的,部署环境和测试环境的差异。即便我们的软件写得完美无缺,环境永远是不可轻视的。除非我们能做到让所有环境完全一致,从头到尾,否则还是那句话,部署要趁早。