QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

利用 Ruby 简化你的 Java 测试

  • 2008-08-26
  • 本文字数:6136 字

    阅读完需:约 20 分钟

本文是 Productive Java with Ruby 系列文章的第一篇,我将从单元测试这个话题开始,让 Java 的开发人员能够在实际工作中利用 Ruby 提高工作效率。

Martin Fowler:当然(愿意花掉一半的时间来写单元测试)!因为单元测试能够使你更快地完成工作。无数次的实践已经证明这一点。你的时间越是紧张,就越是要写单元测试,它看上去慢,但实际上能够帮助你更快、更舒服地达到目标。

单元测试很重要,但是……

单元测试的重要性,我想再多做一些强调也不为过。但实际情况是我经常听到 Java 开发人员抱怨单元测试繁琐、难写。虽然勉强为之,却疲于奔命,并没有体会到它的好处!最终造成的结果是出现了大量只能运行一次的单元测试。是将责任简单归结于开发人员?还是开发流程或制度的不完善?

平心而论,我自己在做 TDD 或单元测试的时候,有很多时候也确实觉得无趣,尤其是在一些准备测试数据或测试环境的工作上,例如我们经常需要随机生成特定长度的字符串用于测试,需要如下代码:

public String getRandomAlphabetic(int count) {<br></br> count = count <= 0 ? 5 : count; // 默认为 5<br></br> // 构建一个包含所有英文字母的字符串 <br></br> String alphabet="abcdefghijklmnopqistuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";<br></br> StringBuffer sb = new StringBuffer(count);<br></br> for (int i = 0; i < count; i++) {<br></br> int character=(int)(Math.random()*26);<br></br> sb.append(alphabet.substring(character, character+1));<br></br> }<br></br> return sb.toString();<br></br> }如果用 Ruby 的话

def random_alphabetic(size=5)<br></br> chars = ('a'..'z').to_a + ('A'..'Z').to_a # 构建一个从 a 到 Z 的一个字母表数组 <br></br> (0...size).collect { chars[rand(chars.length)] }.join # 从 chars 数组中返回指定长度的随机字符数组(默认 5 个),调用 join 方法,将这个数组中的所有元素连接成一个字符串 <br></br> end对比后大家感觉如何?有经验的开发人员马上会挑战说,我们有现成的 commons-lang 库,简单调用 RandomStringUtils.randomAlphabetic(5) 就可以完成任务,可我想问的是,如果没有第三方库的支持,你更愿意用哪种方式?还可以想象构建一个树状结构的数据,Ruby 的方式

<br></br> data =<<-EOF<br></br> {<br></br> "order_id": "xxx-xxxxx-xxx",<br></br> "books": [<br></br> {"ISBN": "2323-2323", "number": 2, "price": 20.00},<br></br> {"ISBN": "2323-2324", "number": 3, "price": 30.00},<br></br> {"ISBN": "2323-2325", "number": 2, "price": 20.00},<br></br> {"ISBN": "2323-2326", "number": 3, "price": 30.00},<br></br> {"ISBN": "2323-2327", "number": 2, "price": 20.00}<br></br> ]<br></br> }<br></br> EOF # 该数据为 json 格式的一段字符串 <p> order = JSON.parse(data)</p><br></br> p order['books'][0]['ISBN'] #=> 2323-2323 <br></br>用 Java 该怎样完成,很多人会祭出 Java 世界中最被滥用的七种武器之首“xml”,即便如此能完成的如此优雅吗?如果不是简单的“语言宗教崇拜”,至少我会毫不犹豫的选择用 Ruby 的方式完成任务。省点时间,早点下班陪陪老婆也好啊!:)

在 Ruby 的世界里

那作为一个 Java 的开发人员,如何享受到 Ruby 在测试方面给我们带来的好处呢?事实上 Java 早就为 Ruby、Python 等脚本语言做好了准备,JRuby 是 sun 对 Ruby on JVM 的官方支持,现在的版本是 1.1.3,已经能够非常好的让 Ruby 运行在 Java 的世界里。

在开始之前,我想简单介绍一下跟本文相关的 Ruby 及 JRuby 的基本用法,具体参考 Ruby Home

# 这是一段注释 <br></br> puts 'Hello, World !' #打印 Hello, World! 这个字符串,相当于 Java 中的 System.out.println<br></br> ('a'..'z') #声明一个 a 到 z 的 range 类型的数据,range 表示一个连续的范围,当然也可以是一段连续的数字 <br></br> ('a'..'z').to_a #简单调用方法 to_a 将一个 range 类型的数据转换成一个数组 <p> <<-EOF</p><br></br> ...<br></br> EOF # Ruby 通过配对 EOF 的方式声明一个多行的字符串 <p> [1,2,3,4,5] #声明一个数组 </p><br></br> [..].each {|it| puts it } #通过 each 方法遍历每个元素,其中{...}表示一个代码块,在这里的语义是在遍历每个元素时打印这个元素,其中 it 是隐式声明的参数,表示当前被遍历到的元素 <br></br> 对于数组可以用 select, find, collect 等方法遍历,用 <<, push, pop, delete 等方法改变数组里的元素 <p> h = {'id'=>'1', 'name'=>'foo', 'age'=>24} #简单声明一个 Hash</p><br></br> h['name'] = 'bar' #对 key 为 name 的条目赋值 <br></br> h['age'] #24<p> class Foo < Base #通过 class 关键字声明一个类,‘< Base’表示从基类 Base 继承 </p><br></br> @name #声明一个实例变量 <br></br> @@count #声明一个类变量,相当于 Java 中 static 关键字修饰的变量 <br></br> end在 JRuby 中可以直接使用所有的 Ruby 类和方法,也能够很轻松的调用 Java 的类库,实际上 JRuby 将 Ruby 代码动态编译成 JVM 的字节码,具体参考 JRuby

<br></br> require 'java' #引入对 Java 的支持 <br></br> import 'java.util.ArrayList' #导入需要的某个包 <p> list = ArrayList.new #创建一个 ArrayList</p><br></br> [1,2,3,4,5].to_java #将 Ruby 类型转成对应的 Java 类型从上面简短的例子和基本介绍,我们能发现什么?Ruby 对数组,字符串等基本类型提供了强大的支持,而这些恰恰是 Java 缺乏的,我们没有办法简单的创建一个数组,不能用简单的方式遍历这些集合,甚至都不能简单声明一个多行的字符串。而这些在进行测试工作,准备测试数据的时候都是必不可少的!利用 Ruby 的这些特性,我们可以极大的增加开发的效率,摆脱相当多繁琐的工作。当然,这些只是 Ruby 为我们提供的诸多好处中最直观的部分,随着我们的讨论深入,我们将看到越来越多有意思的特性。

准备工作

用 Ruby 进行测试,我们需要JtestR这个专门为简化 Java 测试而准备的 Ruby 测试工具,当前的最新版本是 0.3.1。如果你使用maven,在pom.xml中加入

<plugins><p>        ...</p><p>        <plugin></p><p>            <groupId>org.jtestr</groupId></p><p>            <artifactId>jtestr</artifactId></p><p>            <version>0.3.1</version></p><p>            <configuration></p><p>                <!-- Ruby 测试文件所在目录 --></p><p>                <tests>src/test/ruby</tests></p><p>            </configuration></p><p>            <executions></p><p>                <execution></p><p>                    <goals></p><p>                        <goal>test</goal></p><p>                    </goals></p><p>                </execution></p><p>            </executions></p><p>        </plugin></p><p>        ...</p><p>    </plugins></p><br></br>使用 ant 的开发人员请参考这里。用 Ruby 做单元测试和 Java 一样,简单从Test::Unit::TestCase继承即可

<br></br> class MyFirstJRubyTests < Test::Unit::TestCase<br></br> def test_true<br></br> assert true<br></br> end<br></br> end<br></br>可以将这个测试文件简单拷贝到myProj/src/test/ruby目录下,运行mvn test,你会看到JtestR产生的测试结果输出

<br></br> [INFO] [jtestr:test {execution: default}]<p> Other TestUnit: 1 tests, 0 failures, 0 errors</p><br></br>在这段输出报告之上,你应该还能看到正常的 Java unit testcase 输出的测试结果,这表明,我们可以在开发的过程中同时选择用 Java 的方式测试,或用 Ruby 的方式测试!

JRuby 测试之旅

好了,一切准备好之后,就可以开始我们的 Ruby 测试之旅了!你一定不希望自己苦心经营的 blog 或论坛上出现某些“不和谐”的词,尤其是在这举国欢庆的特殊阶段。你设计了一个专门用于过滤带有这些关键服务接口

<br></br> public interface KeywordFilterService {<br></br> // 过滤访客评论字符串数组,返回一个新的不包含敏感关键字的结果 <br></br> String[] filter(String[] comments);<br></br> // 获取被过滤的访客评论 <br></br> String[] getFiltedComments();<br></br> }<br></br>并写了一个很简单的实现类class KeywordFilterServiceImpl implements KeywordFilterService,这个类的实现我们就暂不关心,把重点聚集在如何对这个实现类进行测试上。首先在myProj/src/test/ruby目录下新建test_keyword_filter_service.rb文件,键入以下内容

<br></br> require 'test/unit'<p> class KeywordFilterServiceTest < Test::Unit::TestCase</p><br></br> def setup<br></br> @keywords = %w{X XX XXX XXXX XXXXX XXXXXX XXXXXXX} #不用加引号,更方便 <br></br> end<br></br> def test_filter<p> end</p><br></br> endsetup 方法准备了我们要测试的关键字数据,在 Ruby 中 %w{…}用来简单定义字符串数组。test_xxx 方法就是我们的测试方法。有了关键字数据后我们还需要一组用来测试的测试数据,里面一部分包含我们的关键字。我决定用上面定义的随机生成字符串的方式产生这些测试数据

<br></br> def random_alphabetic(size=5)<br></br> chars = ('a'..'z').to_a + ('A'..'Z').to_a<br></br> (0...size).collect { chars[rand(chars.length)] }.join<br></br> end<p> def random_comments</p><br></br> comments ||= []<br></br> 10.times do<br></br> keyword = rand(10) % 3 == 0 ? ' ' : @keywords[rand(@keywords.length)] #随机决定是否包含关键字 <br></br> comment = random_alphabetic + keyword + random_alphabetic<br></br> comments << comment<br></br> end<br></br> return comments<br></br> end<br></br>这样,每次产生 10 条数据,有近三分之一的数据中包含不和谐的关键字。有了测试数据剩下的工作就很简单了,我们只需调用写好的 Java 服务,对返回的测试数据进行验证即可,由于需要调用 Java 服务,和 Java 一样,我们首先要引入类:

import 'com.alisoft.research.JRuby.service.KeywordFilterServiceImpl'测试方法实现如下:

<br></br> def test_filter<br></br> comments = random_comments<br></br> service = KeywordFilterServiceImpl.new(@keywords.to_java :String)<br></br> filted = service.filter(comments.to_java :String)<p> forbiddens = service.getFiltedComments</p><p> assert forbiddens.length == comments.length - filted.length</p><br></br> assert_equal forbiddens.sort, (comments - filted).sort<br></br> end<br></br>其中,有两点需要注意,首先,我们可以通过to_java方法将 Ruby 类型转换成 Java 类型,例如上面将@keywords.to_java :String表明将 Ruby 数组转换成 Java 的 String 数组。第二,Ruby 对数组支持“-”的操作,表示将一个数组减去和另一个数组中相同的元素,非常的直观!很明显,被过滤的数组应该等于原来的数组减去过滤后的结果!运行mvn test,我们将看到

<br></br> [INFO] [jtestr:test {execution: default}]<p> Other TestUnit: 2 tests, 0 failures, 0 errors</p><br></br>## 结论

说明新增加的测试通过!最后我们来对比一下实现同样的功能 Ruby 和 Java 的差别

<br></br> Ruby:<br></br> require 'test/unit'<p> import 'com.alisoft.research.JRuby.service.KeywordFilterServiceImpl'</p><p> class KeywordFilterServiceTest < Test::Unit::TestCase</p><br></br> def setup<br></br> @keywords = %w{X XX XXX XXXX XXXXX XXXXXX XXXXXXX}<br></br> end<p> def test_filter</p><br></br> comments = random_comments<br></br> service = KeywordFilterServiceImpl.new(@keywords.to_java :String)<br></br> filted = service.filter(comments.to_java :String)<p> forbiddens = service.getFiltedComments</p><p> assert forbiddens.length == comments.length - filted.length</p><br></br> assert_equal forbiddens.sort, (comments - filted).sort<br></br> end<p> def random_alphabetic(size=5)</p><br></br> chars = ('a'..'z').to_a + ('A'..'Z').to_a<br></br> (0...size).collect { chars[rand(chars.length)] }.join<br></br> end<p> def random_comments</p><br></br> comments ||= []<br></br> 10.times do<br></br> keyword = rand(10) % 3 == 0 ? ' ' : @keywords[rand(@keywords.length)]<br></br> comment = random_alphabetic + keyword + random_alphabetic<br></br> comments << comment<br></br> end<br></br> return comments<br></br> end<br></br> end<p> Java:</p><br></br> package com.alisoft.research.JRuby.test;<p> import static org.junit.Assert.assertArrayEquals;</p><br></br> import static org.junit.Assert.assertEquals;<p> import Java.util.ArrayList;</p><br></br> import Java.util.Arrays;<br></br> import Java.util.List;<p> import org.apache.commons.lang.RandomStringUtils;</p><br></br> import org.apache.commons.lang.math.RandomUtils;<br></br> import org.junit.Test;<p> import com.alisoft.research.JRuby.service.KeywordFilterServiceImpl;</p><p> public class KeywordFilterServiceTest {</p><p> @Test</p><br></br> public void testFilteredResults() {<br></br> String[] comments = getRandomComments();<p> KeywordFilterServiceImpl service = new KeywordFilterServiceImpl(</p><br></br> getKeywords());<p> String[] filted = service.filter(comments);</p><br></br> String[] forbiddens = service.getFiltedComments();<p> assertEquals(filted.length + forbiddens.length, comments.length);</p><p> assertArrayEquals(forbiddens, sub(comments, filted));</p><br></br> }<br></br> // 实现减法操作 <br></br> private String[] sub(String[] all, String[] part) {<br></br> List<string> allList = new ArrayList<string>(Arrays.asList(all));<br></br> allList.removeAll(Arrays.asList(part));<br></br> return allList.toArray(new String[allList.size()]);<br></br> }<p> private String[] getRandomComments() {</p><br></br> String[] comments = new String[RandomUtils.nextInt(10)];<br></br> for (int i = 0; i < comments.length; i++) {<br></br> String comment = RandomStringUtils.randomAlphabetic(5);<br></br> String keyword = RandomUtils.nextBoolean() ? getKeywords()[RandomUtils<br></br> .nextInt(getKeywords().length)]<br></br> : "";<br></br> comment += keyword + RandomStringUtils.randomAlphabetic(5);<br></br> comments[i] = comment;<br></br> }<br></br> return comments;<br></br> }<p> private String[] getKeywords() {</p><br></br> String[] keywords = new String[] { "X", "XX", "XXX", "XXXX",<br></br> "XXXXX", "XXXXXX", "XXXXXXX" };<br></br> return keywords;<br></br> }<p> }</p></string></string>在借助了apache-commons-lang之后,LOC: Java 58, Ruby 35。大家也可以注意一下 Java 中实现两个数组“减法”的代码对比 Ruby 的实现,Ruby 明显更为直观,更有效率!

利用 Ruby 对 Java 进行测试的基础介绍就到这里,希望能抛砖引玉,引起大家的兴趣。下一篇我将和大家再讨论一些例如 mock 等更高级的测试话题。


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

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

2008-08-26 22:592487

评论

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

LeetCode题解:217. 存在重复元素,哈希表,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

架构实战营毕业总结

俞立夫

架构实战营

MySQL 系列教程之(六)DML 操作:数据的增删改

若尘

数据库 MySQL 数据库 8月日更

使用账号密码来操作github? NO!

程序那些事

Java GitHub 程序那些事

多线程、分布式、高并发都不懂?你拿什么跳槽?

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

底层即真理!Netty+Redis+ZooKeeper解读高并发架构

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

业界良心啊!第五次更新的Spring Cloud Alibaba升级太多内容

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

webrtc AlrDetector

webrtc developer

linux工具之TC

webrtc developer

二本渣渣5面阿里,从准备简历到“直怼”面试官,经历了什么?

Java 程序员 架构 面试 计算机

去哪儿网数据同步平台技术演进与实践

Qunar技术沙龙

数据库 数据中台 ES 数据同步 Kafk

centos8 mediasoup 搭建

webrtc developer

WebRTC mediasoup

“区块链”赋能智慧社区,多维度提升管理质效

旺链科技

区块链 智慧社区

基于KubeEdge实现中国移动10086客服云边协同平台

华为云原生团队

云计算 开源 运维 边缘计算 边缘技术

从“人工”到“人工智能”,聊一聊本届东京奥运会的AI黑科技

行者AI

千亿级模型在离线一致性保障方案详解

百度Geek说

百度 测试 后端

腾讯WeTest零售行业质量解决方案

WeTest

终于有人把TCP协议与UDP协议给搞明白了

编程菌

Java 编程 程序员 计算机 java技术宅

云原生 | 混沌工程工具 ChaosBlade Operator Node 篇

RadonDB

数据库 云原生 混沌工程

价值连城 图灵奖得主Yoshua Bengio约书亚·本吉奥的采访 给AI从业者的建议 John 易筋 ARTS 打卡 Week 60

John(易筋)

ARTS 打卡计划

EMQ 映云科技成为开源项目 Vue.js 定期捐赠者

EMQ映云科技

Java 开源 大前端 emq

太为难我了,阿里面试了7轮(5年经验,拿下P7岗offer)

Java 编程 程序员 架构 面试

银行小程序隐私安全如何做?诊疗一体,一步到位

WeTest

好评如潮,PerfDog两年迭代正式启动商业化探索

WeTest

架构师训练营-毕业设计

俞立夫

IM开发技术学习:揭秘微信朋友圈这种信息推流背后的系统设计

JackJiang

即时通讯 IM 微信朋友圈

机会!痛点!难点!中国游戏泛娱乐企业出海攻略全解析

环信

游戏出海 直播 社交APP出海 泛娱乐社交

多张图片的形式

冇先生

给Arm生态添把火,腾讯Kona JDK Arm架构优化实践

腾源会

开源 腾讯 jdk 腾讯开源 KonaJDK

堡垒机品牌就认行云管家!为什么呢?

行云管家

云计算 系统运维 堡垒机 IT运维 云计算运维

云时代的到来会淘汰运维人员吗?运维工作可以一直做吗?

行云管家

云计算 运维 云服务 IT运维 云时代

利用Ruby简化你的Java测试_Java_殷安平_InfoQ精选文章