写点什么

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

  • 2008-09-22
  • 本文字数:4354 字

    阅读完需:约 14 分钟

——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<br></br> map.expects(:size).returns(5) #=> 模拟并期望调用 size 方法时返回 5<br></br> assert_equal 5, map.size        #=> 断言,和 JUnit 断言非常相似 EasyMock 是个流行的开源 Java Mock 测试框架,在它的官方网站的文档中刚好有如何利用Mock 进行测试的示例,为了方便说明,我将直接引用这个示例,并用JRuby 实现基于Mock 的测试。首先我们有一个接口:

// 协作者接口,用以跟踪协作文档的相关状态 <br></br>public interface Collaborator {<br></br> void documentAdded(String title); // 当新增文档时触发 <br></br> void documentChanged(String title); // 当文档改变时触发 <br></br> void documentRemoved(String title); // 当文档被删除时触发 <br></br> byte voteForRemoval(String title); // 当文档被共享,并进行删除操作是,执行投票的动作 <br></br> byte[] voteForRemovals(String[] title); // 同上,不过可以同时投票多个文档 <br></br>}在这个示例中,还有一个ClassUnderTest类实现了管理协作文档的相关逻辑,简化示例代码如下:

public class ClassUnderTest {<br></br> // ... <br></br> public void addListener(Collaborator listener) {<br></br> // 增加协作者 <br></br> }<br></br> public void addDocument(String title, byte[] document) { <br></br> // ... <br></br> }<br></br> public boolean removeDocument(String title) {<br></br> // ... <br></br> }<br></br> public boolean removeDocuments(String[] titles) {<br></br> // ... <br></br> }<br></br>}到这里开始,我们就可以开始利用 JRuby 进行测试了。上一篇中我介绍了Ruby 的测试框架,不过这次,我们学习一个新的测试框架 dust ,它可以让你以更简洁的方式书写测试:

import "org.easymock.samples.ClassUnderTest"<br></br> import "org.easymock.samples.Collaborator"<br></br> unit_tests do<br></br>     cut = ClassUnderTest.new<br></br>     mock = Collaborator.new #=> mock 一个接口只需直接 new 出来即可 <br></br>     cut.addListener(mock)<br></br>#测试方法以 test 开始,后面跟一段具有描述性的字符串,然后在 block 中完成测试逻辑 <br></br>     test "001 remove none existing document" do<br></br>         cut.removeDocument("Does not exist")<br></br>     end<br></br> end将上述代码拷贝至src/test/ruby下,运行mvn test命令,OK,通过了相关测试。非常简单吧! dust 甚至让我们不用声明任何类就可以开始工作了,处处都体现着 ruby 简单、高效的理念!

加速

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

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

test "002 add document" do<br></br>         mock.expects(:documentAdded).with("New Document") #=> 我们期待 documentAdded 被执行,并且 title 的值为“New Document”<br></br>         <br></br>         cut.addDocument("New Document", [])<br></br>     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<br></br> public void addDocument() {<br></br>     mock.documentAdded("New Document");<br></br>     replay(mock);<br></br>     classUnderTest.addDocument("New Document", new byte[0]);<br></br>     verify(mock);<br></br> }Ruby 代码还是稍稍比 Java 代码简洁一些,虽然优势不明显。我们继续完成后续的测试,增加并改变一个文档:

test "003 add and change document" do<br></br>     mock.expects(:documentAdded).with("Document")<br></br>     #在 ClassUnderTest 实现逻辑中,后续增加的同名文档属于修改操作,所以 documentChanged 事件被触发了三次 <br></br>     mock.expects(:documentChanged).with("Document").times(3)  #=> DSL here<p>     cut.addDocument("Document", [].to_java(:byte))</p><br></br>     cut.addDocument("Document", [].to_java(:byte))<br></br>     cut.addDocument("Document", [].to_java(:byte))<br></br>     cut.addDocument("Document", [].to_java(:byte))<br></br> end运行测试,全部通过!请大家注意mock.expects(..).with(..).times(3)这行代码,代码本身似乎就在说我期望这个对象的 XXX 方法被调用,参数是 xx,并且一共被调用了 3 次。书写简洁,阅读也非常的语义化!这就是我们所说的 DSL(Domain Specific Language), mocha 就是 Ruby 在 Mock 测试方面的领域化语言!它支持的语义非常的丰富,包括:

<span>at_least</span>   <span>at_least_once</span>   <span>at_most</span>   <span>at_most_once</span>   <span>in_sequence</span>   <span>never</span>   <span>once</span>   <span>raises</span>   <span>returns</span>   <span>then</span>   <span>times</span>   <span>when</span> 等等。DSL 的应用是 Ruby 的一大特点,它甚至能让我们写出连客户都能很容易看懂的测试代码。这在敏捷实践中,与用户讨论接收测试时就显得非常有用及必要!我们也同样对比一下 JUnit 和 EasyMock 的实现:p @Test<br></br> public void addAndChangeDocument() {<br></br>    mock.documentAdded("Document");<br></br>    mock.documentChanged("Document");<br></br>    expectLastCall().times(3);<br></br>    replay(mock);<br></br>         <br></br>     classUnderTest.addDocument("Document", new byte[0]);<br></br>     classUnderTest.addDocument("Document", new byte[0]);<br></br>     classUnderTest.addDocument("Document", new byte[0]);<br></br>     classUnderTest.addDocument("Document", new byte[0]);<br></br>     verify(mock);<br></br> }EasyMock 属于非常正常的 API 调用,没有太多 DSL 的概念,在这方面 JMock 相对来说要好一些,不过和 Ruby 相比,表达相同的语义,还是更繁琐一些。我们继续完成最后一段测试代码,删除及投票:

test "004 vote for removel" do<br></br>     mock.expects(:voteForRemoval).with("Document").returns(42)<br></br>     mock.expects(:documentRemoved).with("Document")<br></br>     assert_equal true, cut.removeDocument("Document")<br></br> 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 中文站用户讨论组参与我们的线上讨论。

2008-09-22 02:322570

评论

发布
暂无评论
发现更多内容

.NET源码解读kestrel服务器及创建HttpContext对象流程

不在线第一只蜗牛

.net 开发语言

从规划、预算和预测中实现企业未来价值

智达方通

财务分析 预测模型 业财融合 全面预算管理

Jane Street简街进军大陆不良资产处置

科技热闻

Hibernate5 动态模型

alexgaoyh

hibernate jpa 动态模型 运行时 sessionfactory

传统全面预算管理体系暴露的问题

用友BIP

全面预算

千万不能小瞧的PCB半孔板

华秋PCB

科普 电路 PCB PCB设计 涨知识

ABAQUS 模拟过盈配合解决材料选择及公差带设计等问题

思茂信息

仿真软件 仿真技术 abaqus abaqus软件 abaqus有限元仿真

基于多尺度图神经网络的流场预测,实现精度与速度的平衡

飞桨PaddlePaddle

人工智能 百度飞桨 开发者说

【Netty】「优化进阶」(二)浅谈 LengthFieldBasedFrameDecoder:如何实现可靠的消息分割?

sidiot

Java 后端 Netty 6 月 优质更文活动

滴滴开源的深度与温度——滴滴开源委员会秘书长吴国斌专访

开源雨林

开源 滴滴 滴滴夜莺 开源治理

究竟是什么让山东航空人力资源数智化转型如虎添翼?

用友BIP

数智人力

NFTScan 与 Wagmi Labs 达成合作伙伴,双方在多链 NFT 数据方面展开合作!

NFT Research

NFT\

【Netty】「优化进阶」(三)Netty 通信协议设计:从 Redis、HTTP 和自定义协议看起

sidiot

Java 后端 Netty 6 月 优质更文活动

华为云GaussDB为MetaERP“成本核算”产品“保驾护航”

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 6 月 PK 榜

基于OpenHarmony开发的玻璃拟态天气应用(2)构建当前天气组件

路北路陈

前端 HarmonyOS OpenHarmony应用开发 6 月 优质更文活动

【Netty】「源码解析」(一)ByteBuf 的动态扩容策略与实现原理

sidiot

Java 后端 Netty 6 月 优质更文活动

Maya软件是做什么的?Maya软件优缺点分析!

Finovy Cloud

maya

企业如何提高全面提升预算管理效率?

用友BIP

全面预算

【Netty】「优化进阶」(一)粘包半包问题及解决方案

sidiot

Java 后端 Netty 6 月 优质更文活动

低代码平台的实现原理

互联网工科生

软件开发 低代码 JNPF

从两个案例看Apache Flink如何提升企业实时数据处理效率

xfgg

Java flink 6 月 优质更文活动

汇聚漫天星光,点燃HarmonyOS之火,2023HSD校园行·清华大学站圆满收官

最新动态

IoTLink版本更新V1.29.0

山东云则信息科技

助力金融信创!索信达携手麒麟软件、GBASE南大通用发布联合解决方案

索信达控股

企业数智化转型数智员工到底能做什么?

用友BIP

数智底座 Pass平台

重磅国赛来了!2023中国高校计算机大赛-人工智能创意赛启动报名

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨

【Netty】「源码解析」(二)HeapBuffer 创建过程详解:高效可靠的内存管理技巧

sidiot

Java 后端 Netty 6 月 优质更文活动

中企出海之营销最佳实践——新加坡SK集团

用友BIP

中企出海

利用Ruby简化你的Java测试(进阶篇)_Java_殷安平_InfoQ精选文章