InfoQ

InfoQ

文章

我的书签

登录注册 以永久保存书签。

该内容已经被标记书签!

标记书签错误,请重试!

利用Ruby简化你的Java测试(进阶篇)

作者 殷安平 发布于 2008年9月22日

领域
语言 & 开发
主题
Ruby ,
Java ,
单元测试
标签
Mocks ,
测试 ,
JRuby

——Productive Java with Ruby系列文章(二)

本文是Productive Java with Ruby系列文章的第二篇,通过上一篇的介绍,我想大家对如何利用Ruby进行单元测试有了一个基本的了解,从这里开始,我将和大家一起讨论一些利用Ruby进行单元测试时的高级话题。

通常,新技术的引入只能降低解决问题的难度,而不是消除问题本身!

在“依赖”的原始丛林中挣扎...

通过Ruby我们可以更高效的处理数据准备的问题,但是真实的世界并不那么简单!随着测试的深入,我们会越发的感觉一不小心就挣扎在“依赖”的原始丛林中!有时候似乎需要加入无数的jar包,初始化所有的组件,配置完一切的数据库、服务器及网络的关系,才能开始一小段简单的测试。更痛苦的是这一切是如此的脆弱,仅仅是某人在数据库中多加了一条数据或者更改了一部分环境配置,你苦心构建的所有测试就全部罢工了!多少次,你仰天长叹:“神啊!救救我吧...”。可神在那里呢?

Mock

单元测试之所以有效,是因为我们遵从了快速反馈,小步快跑的原则!一次只测试一件事情!而大量依赖的解决工作明显让单元测试偏离的原本的目标,也让人觉得不舒服。Mock技术就能让我们有效摆脱在丛林中的噩梦。我们知道,在计算机的世界里,同样的输入一定能得到对应的输出,否则就是异常情况了。Mock技术本质上是通过拦截并替换指定方法的返回值摆脱对程序实现的依赖。对于1+1这样的输入条件进行计算,Mock技术直接拦截原方法,替换该计算方法的返回值为2,不关心这个算法到底是通过网络得到的,还是通过本地计算得到的。这样就和具体实现解藕了。

在对Java进行单元测试的时候,通常会对某个具体类或某个接口产生依赖,要解藕就需要能够对具体类或接口进行Mock。幸好这些在JRuby中都非常的简单,由于JtestR自动为我们引入了mocha这个Mock框架,让我们可以更简单的开始工作。先看一个针对HashMap的Mock测试吧:

map = mock(HashMap)           #=> mock java.util.HashMap类,如果是接口可以直接new出来,例如Map.new
map.expects(:size).returns(5) #=> 模拟并期望调用size方法时返回5
assert_equal 5, map.size        #=>断言,和JUnit断言非常相似

EasyMock是个流行的开源Java Mock测试框架,在它的官方网站的文档中刚好有如何利用Mock进行测试的示例,为了方便说明,我将直接引用这个示例,并用JRuby实现基于Mock的测试。首先我们有一个接口:

//协作者接口,用以跟踪协作文档的相关状态
public interface Collaborator {
void documentAdded(String title); //当新增文档时触发
void documentChanged(String title); //当文档改变时触发
void documentRemoved(String title); //当文档被删除时触发
byte voteForRemoval(String title); //当文档被共享,并进行删除操作是,执行投票的动作
byte[] voteForRemovals(String[] title); //同上,不过可以同时投票多个文档
}

在这个示例中,还有一个ClassUnderTest类实现了管理协作文档的相关逻辑,简化示例代码如下:

public class ClassUnderTest {
// ...
public void addListener(Collaborator listener) {
// 增加协作者
}
public void addDocument(String title, byte[] document) {
// ...
}
public boolean removeDocument(String title) {
// ...
}
public boolean removeDocuments(String[] titles) {
// ...
}
}

到这里开始,我们就可以开始利用JRuby进行测试了。上一篇中我介绍了Ruby的测试框架,不过这次,我们学习一个新的测试框架dust,它可以让你以更简洁的方式书写测试:

import "org.easymock.samples.ClassUnderTest"
import "org.easymock.samples.Collaborator"
unit_tests do
    cut = ClassUnderTest.new
    mock = Collaborator.new #=> mock一个接口只需直接new出来即可
    cut.addListener(mock)
#测试方法以test开始,后面跟一段具有描述性的字符串,然后在block中完成测试逻辑
    test "001 remove none existing document" do
        cut.removeDocument("Does not exist")
    end
end

将上述代码拷贝至src/test/ruby下,运行mvn test命令,OK,通过了相关测试。非常简单吧!dust甚至让我们不用声明任何类就可以开始工作了,处处都体现着ruby简单、高效的理念!

加速

跑过几次单元测试后,大家一定会发现测试代码是很容易书写,但是跑测试的时间似乎有点长!难道JRuby的性能这么差?其实整个测试过程中启动JRuby花费了很多时间,JtestR框架也考虑的很周到,只需要启动一个本地的测试服务器就可以大大加快测试执行的速度,在shell中执行mvn jtestr:server即可。再跑一次单元测试,速度大大增加了吧!

上面的代码只测试了删除一个不存在的文档,逻辑太过简单,不能说明任何问题,我们继续后面的测试,新增一个文档:

    test "002 add document" do
        mock.expects(:documentAdded).with("New Document") #=> 我们期待documentAdded被执行,并且title的值为“New Document”
       
        cut.addDocument("New Document", [])
    end

运行测试,居然出错了,TypeError: for method addDocument expected [java.lang.String, [B]; got: [java.lang.String,org.jruby.RubyArray,原来错在cut.addDocument("New Document", [])的方法中我简单传入了[],这是一个Ruby数组对象,将这段代码改成:

cut.addDocument("New Document", [].to_java(:byte))

重新运行测试,OK,全部通过。在JRuby中进行测试时调用Java对象的方法要注意将Ruby对象转换成Java对象。我们对比一下JUnit的代码

@Test
public void addDocument() {
    mock.documentAdded("New Document");
    replay(mock);
    classUnderTest.addDocument("New Document", new byte[0]);
    verify(mock);
}

Ruby代码还是稍稍比Java代码简洁一些,虽然优势不明显。我们继续完成后续的测试,增加并改变一个文档:

test "003 add and change document" do
    mock.expects(:documentAdded).with("Document")
    #在ClassUnderTest实现逻辑中,后续增加的同名文档属于修改操作,所以documentChanged事件被触发了三次
    mock.expects(:documentChanged).with("Document").times(3)  #=> DSL here

    cut.addDocument("Document", [].to_java(:byte))
    cut.addDocument("Document", [].to_java(:byte))
    cut.addDocument("Document", [].to_java(:byte))
    cut.addDocument("Document", [].to_java(:byte))
end

运行测试,全部通过!请大家注意mock.expects(..).with(..).times(3)这行代码,代码本身似乎就在说我期望这个对象的XXX方法被调用,参数是xx,并且一共被调用了3次。书写简洁,阅读也非常的语义化!这就是我们所说的DSL(Domain Specific Language),mocha就是Ruby在Mock测试方面的领域化语言!它支持的语义非常的丰富,包括:

  at_least   at_least_once   at_most   at_most_once   in_sequence   never   once   raises   returns   then   times   when 
等等。DSL的应用是Ruby的一大特点,它甚至能让我们写出连客户都能很容易看懂的测试代码。这在敏捷实践中,与用户讨论接收测试时就显得非常有用及必要!我们也同样对比一下JUnit和EasyMock的实现:p
@Test
public void addAndChangeDocument() {
   mock.documentAdded("Document");
   mock.documentChanged("Document");
   expectLastCall().times(3);
   replay(mock);
       
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    verify(mock);
}

EasyMock属于非常正常的API调用,没有太多DSL的概念,在这方面JMock相对来说要好一些,不过和Ruby相比,表达相同的语义,还是更繁琐一些。我们继续完成最后一段测试代码,删除及投票:

test "004 vote for removel" do
    mock.expects(:voteForRemoval).with("Document").returns(42)
    mock.expects(:documentRemoved).with("Document")
    assert_equal true, cut.removeDocument("Document")
end

看到这里,细心的同学一定会发现有些奇怪,并没有先增加一个Tilte是Document呀?是的,这个是Ruby的单元测试和Java机制不一样的地方,JUnit中,每个方法是在线程中执行的,不保证被执行的先后顺序,而Ruby的单元测试是简单反射,按字母排序后执行的,所以只有一个上下文环境。我特意在每个方法的描述前加了个数字序列,以保证按这个数字的大小顺序执行!

好了,到这里,对利用Ruby进行Mock测试介绍基本完成!剩余的EasyMock的示例测试留给大家自己完成吧!

总结

引入Ruby进行Mock测试可以有效简化单元测试时对各种环境的依赖,但是Mock也有Mock自己的问题,例如,它需要你对被测试类的内部细节有一定的了解,毕竟利用Mock技术进行测试属于白盒测试。当被测试类的内部实现有所改变而外部接口未发生变化时,原本不该出错的测试方法依旧有被打破的风险。还是回到开篇的那句话:通常,新技术的引入只能降低解决问题的难度,而不是消除问题本身!

相关阅读:Productive Java with Ruby系列文章(一):利用Ruby简化你的Java测试


作者介绍:殷安平,现任阿里软件研究院平台二部架构师,工作6年以来一直从事Java开发,爱好广泛,长期关注敏捷开发。对动态语言有了强烈的兴趣,致力于将动态语言带入实际工作中!工作之余喜欢摄影和读书。个人RSS聚合: http://friendfeed.com/yapex。联系方式:anping.yin AT alibaba-inc.com。

志愿参与InfoQ中文站内容建设,请邮件至editors@cn.infoq.com。也欢迎大家到InfoQ中文站用户讨论组参与我们的线上讨论。

交流 发表人 chenl ray 发表于
  1. 返回顶部

    交流

    发表人 chenl ray

    周四参加了分享,开阔了眼界,作为一个Java测试工程师,在测试过程中用的都是junit、dbunit、httpunit、spring context等框架(较少使用Mock方式),JRuby不知有无相应的解决方案?

深度内容

大规模视频网站的计费与流量管理

本次分享将会就大规模视频网站的计费与流量管理这个话题,从操作层面细细进行讲解和分析,为系统工程师们揭示平日里我们没有关心的另一些内容。同时也希望本次分享能揭示行业中的一些“潜规则”,让互联网行业的流量与带宽管理更为开放与简洁。
本次演讲视频录制于QCon杭州2011

专访Jeffrey Richter:Windows 8是微软的重中之重

Jeffrey Richter以其多本Windows核心技术的经典著作而闻名,同时,他深入掌握微软的.NET等一系列核心技术,2012年1月,Jeffrey Richter在北京接受了InfoQ中文站的专访,谈到Windows 8和WinRT编程,并就异步编程、Windows编程中的可扩展性、性能和安全性方面给出自己的建议。

应用云平台的可用性——从新浪SAE看云平台设计

云计算平台的可用性,相比传统互联网服务而言,更加复杂和困难,也更具有挑战性。本文借助新浪SAE云平台为读者讲述了云平台可用性的定义、如何打造高可用的平台,以及对云计算的用户提出了建议。

JVM定制改进 @ 淘宝

淘宝高度重视Java平台的健康发展,组建了一个团队专注于Java平台的底层部分的性能、功能与稳定性改进;工作主要基于OpenJDK中的HotSpot VM开展,其中一些通用的功能随后也会逐渐反馈给OpenJDK社区。希望能与使用Java平台开发应用的大家交流经验。
本次演讲视频录制于QCon杭州2011

"伤得起"的云计算应用——对云端应用之架构的思考

2011年4月21日至22日是值得云计算从业者纪念的日子。Amazon的IaaS服务出现故障,导致许多商业网站的服务中断,影响非常严重。作为云计算用户,我们需要思考的是,如何保证即便在云服务不可用的情况,我们的应用架构仍然能够屹立不倒?本文正是站在云计算用户的角度试图探讨这一问题。

让交付的速度跟上思考的速度

12人的技术团队,4组刀片服务器,每月20亿的访问量,每日1次准时部署,99.9%的可用性。这可能吗?当然。想知道如何做的吗?百姓网将与您分享他们在DevOps实践过程中的经验和技巧。
本次演讲视频录制于QCon杭州2011

架构之路——穿行在产品和业务之间

篱笆作为一家起源于社区的电子商务公司,反映到技术层面就是同时要面对产品和业务,以及经营战略的变化调整。如何在产品和业务的夹缝之间完成技术架构的抽象与平衡,寻找更有效的价值定位,这当中有些经验教训和个人感悟愿与众人分享。
本次演讲视频录制于QCon杭州2011

特性注入:成功三部曲

本文将对特性注入以及相关方法做一个扫盲性的介绍。我们会解释这个框架的关键要素,并附上实例来证实它们。为了让文章保持相对较短,我们不会深入到某个工具或方法中,而是会给出一些参考资料,以便大家做进一步的研究。