写点什么

利用 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:322692

评论

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

启动!中交集团携手用友搭建海外人力资源数智化平台

用友BIP

中企出海 数智人力

WorkPlus国产化即时通讯工具:安全可靠的国内沟通利器

BeeWorks

企业门户的必备选择,WorkPlus的定制化解决方案

BeeWorks

鲲鹏DevKit 23.0:流水线中便捷迭代鲲鹏版本,迁移、开发、调优无缝衔接

彭飞

JPEX事件对于香港加密货币发展的影响

区块链软件开发推广运营

数字藏品开发 dapp开发 区块链开发 链游开发 NFT开发

用友全球司库十问(四)|企业如何实现融资债券数据信息的实时监控?

用友BIP

全球司库

API接口安全运营研究

Noah

API接口文档 API 安全 API接口安全

小程序等轻应用技术是不是对企业有价值?

Onegun

小程序 轻应用

亚马逊云科技2023柏林峰会主题演讲总结

亚马逊云科技 (Amazon Web Services)

Amazon AIGC

安全高效的政企内部通讯解决方案WorkPlus IM平台

BeeWorks

【数据库审计】2023年数据库审计厂家汇总

行云管家

数据库 数据库审计 等保合规

百万架构师亲码的亿级流量下的分布式限流解决方案

小小怪下士

Java 程序员 高并发 秒杀

深入理解树状数组 | 京东物流技术团队

京东科技开发者

bit #数据结构 企业号10月PK榜 树状数组

WorkPlus即时通讯app打通业务与生态,实现高效管理与协同

BeeWorks

MQ技术比较

周晓宁

中国机械总院张红新:强化集团级数据治理 业财融合助力企业降本增效

用友BIP

业财融合 2023全球商业创新大会

好用的图书阅读器 OmniReader Pro激活中文版

胖墩儿不胖y

Mac软件 图书阅读工具 图书阅读

优化模型之“显示置信度”

矩视智能

深度学习 机器视觉

开源生态建设,正成为智能世界发展的关键

新消费日报

可视大盘+健康分机制,火山引擎DataLeap为企业降低资源优化门槛!

字节跳动数据平台

大数据 数据治理 成本治理 企业号9月PK榜

利用美国服务器增加线上业务的客户人群

一只扑棱蛾子

服务器 美国服务器

WorkPlus定制化的局域网会议软件,提供安全稳定的会议体验

BeeWorks

云管理平台基本功能有哪些?适配国产化平台吗?

行云管家

云计算 信创 国产化 云管平台 云管理

第一个程序:HelloWorld——IDEA 使用

小齐写代码

2023 KiCon Asia 11月12日 深圳见!

华秋电子

kicad

当 AI 成为“逆子”;强化学习之父联手传奇程序员丨 RTE 开发者日报 Vol.62

声网

超越React,JS代码体积减少90%!它为何是2023年最好的Web框架?

互联网工科生

Vue React Web框架 Astro

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