InfoQ

InfoQ

文章

我的书签

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

该内容已经被标记书签!

标记书签错误,请重试!

利用Ruby简化你的Java测试

作者 殷安平 发布于 2008年8月26日

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

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

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

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

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

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

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

如果用Ruby的话

    def random_alphabetic(size=5)
chars = ('a'..'z').to_a + ('A'..'Z').to_a # 构建一个从a到Z的一个字母表数组
(0...size).collect { chars[rand(chars.length)] }.join # 从chars数组中返回指定长度的随机字符数组(默认5个),调用join方法,将这个数组中的所有元素连接成一个字符串
end

对比后大家感觉如何?有经验的开发人员马上会挑战说,我们有现成的commons-lang库,简单调用RandomStringUtils.randomAlphabetic(5)就可以完成任务,可我想问的是,如果没有第三方库的支持,你更愿意用哪种方式?还可以想象构建一个树状结构的数据,Ruby的方式


data =<<-EOF
{
"order_id": "xxx-xxxxx-xxx",
"books": [
{"ISBN": "2323-2323", "number": 2, "price": 20.00},
{"ISBN": "2323-2324", "number": 3, "price": 30.00},
{"ISBN": "2323-2325", "number": 2, "price": 20.00},
{"ISBN": "2323-2326", "number": 3, "price": 30.00},
{"ISBN": "2323-2327", "number": 2, "price": 20.00}
]
}
EOF # 该数据为json格式的一段字符串

order = JSON.parse(data)
p order['books'][0]['ISBN'] #=> 2323-2323

用Java该怎样完成,很多人会祭出Java世界中最被滥用的七种武器之首“xml”,即便如此能完成的如此优雅吗?如果不是简单的“语言宗教崇拜”,至少我会毫不犹豫的选择用Ruby的方式完成任务。省点时间,早点下班陪陪老婆也好啊!:)

在Ruby的世界里

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

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

   # 这是一段注释
puts 'Hello, World !' #打印Hello, World!这个字符串,相当于Java中的System.out.println
('a'..'z') #声明一个a到z的range类型的数据,range表示一个连续的范围,当然也可以是一段连续的数字
('a'..'z').to_a #简单调用方法to_a将一个range类型的数据转换成一个数组

<<-EOF
...
EOF # Ruby通过配对EOF的方式声明一个多行的字符串

[1,2,3,4,5] #声明一个数组
[..].each {|it| puts it } #通过each方法遍历每个元素,其中{...}表示一个代码块,在这里的语义是在遍历每个元素时打印这个元素,其中it是隐式声明的参数,表示当前被遍历到的元素
对于数组可以用select, find, collect等方法遍历,用<<, push, pop, delete等方法改变数组里的元素

h = {'id'=>'1', 'name'=>'foo', 'age'=>24} #简单声明一个Hash
h['name'] = 'bar' #对key为name的条目赋值
h['age'] #24

class Foo < Base #通过class关键字声明一个类,‘< Base’表示从基类Base继承
@name #声明一个实例变量
@@count #声明一个类变量,相当于Java中static关键字修饰的变量
end

在JRuby中可以直接使用所有的Ruby类和方法,也能够很轻松的调用Java的类库,实际上JRuby将Ruby代码动态编译成JVM的字节码,具体参考JRuby


require 'java' #引入对Java的支持
import 'java.util.ArrayList' #导入需要的某个包

list = ArrayList.new #创建一个ArrayList
[1,2,3,4,5].to_java #将Ruby类型转成对应的Java类型

从上面简短的例子和基本介绍,我们能发现什么?Ruby对数组,字符串等基本类型提供了强大的支持,而这些恰恰是Java缺乏的,我们没有办法简单的创建一个数组,不能用简单的方式遍历这些集合,甚至都不能简单声明一个多行的字符串。而这些在进行测试工作,准备测试数据的时候都是必不可少的!利用Ruby的这些特性,我们可以极大的增加开发的效率,摆脱相当多繁琐的工作。当然,这些只是Ruby为我们提供的诸多好处中最直观的部分,随着我们的讨论深入,我们将看到越来越多有意思的特性。

准备工作

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

<plugins>

        ...

        <plugin>

            <groupId>org.jtestr</groupId>

            <artifactId>jtestr</artifactId>

            <version>0.3.1</version>

            <configuration>

                <!-- Ruby测试文件所在目录 -->

                <tests>src/test/ruby</tests>

            </configuration>

            <executions>

                <execution>

                    <goals>

                        <goal>test</goal>

                    </goals>

                </execution>

            </executions>

        </plugin>

        ...

    </plugins>

使用ant的开发人员请参考这里。用Ruby做单元测试和Java一样,简单从Test::Unit::TestCase继承即可


class MyFirstJRubyTests < Test::Unit::TestCase
def test_true
assert true
end
end

可以将这个测试文件简单拷贝到myProj/src/test/ruby目录下,运行mvn test,你会看到JtestR产生的测试结果输出


[INFO] [jtestr:test {execution: default}]

Other TestUnit: 1 tests, 0 failures, 0 errors

在这段输出报告之上,你应该还能看到正常的Java unit testcase输出的测试结果,这表明,我们可以在开发的过程中同时选择用Java的方式测试,或用Ruby的方式测试!

JRuby测试之旅

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


public interface KeywordFilterService {
//过滤访客评论字符串数组,返回一个新的不包含敏感关键字的结果
String[] filter(String[] comments);
//获取被过滤的访客评论
String[] getFiltedComments();
}

并写了一个很简单的实现类class KeywordFilterServiceImpl implements KeywordFilterService,这个类的实现我们就暂不关心,把重点聚集在如何对这个实现类进行测试上。首先在myProj/src/test/ruby目录下新建test_keyword_filter_service.rb文件,键入以下内容


require 'test/unit'

class KeywordFilterServiceTest < Test::Unit::TestCase
def setup
@keywords = %w{X XX XXX XXXX XXXXX XXXXXX XXXXXXX} #不用加引号,更方便
end
def test_filter

end
end

setup方法准备了我们要测试的关键字数据,在Ruby中%w{...}用来简单定义字符串数组。test_xxx方法就是我们的测试方法。有了关键字数据后我们还需要一组用来测试的测试数据,里面一部分包含我们的关键字。我决定用上面定义的随机生成字符串的方式产生这些测试数据


def random_alphabetic(size=5)
chars = ('a'..'z').to_a + ('A'..'Z').to_a
(0...size).collect { chars[rand(chars.length)] }.join
end

def random_comments
comments ||= []
10.times do
keyword = rand(10) % 3 == 0 ? ' ' : @keywords[rand(@keywords.length)] #随机决定是否包含关键字
comment = random_alphabetic + keyword + random_alphabetic
comments << comment
end
return comments
end

这样,每次产生10条数据,有近三分之一的数据中包含不和谐的关键字。有了测试数据剩下的工作就很简单了,我们只需调用写好的Java服务,对返回的测试数据进行验证即可,由于需要调用Java服务,和Java一样,我们首先要引入类:

    import 'com.alisoft.research.JRuby.service.KeywordFilterServiceImpl'

测试方法实现如下:


def test_filter
comments = random_comments
service = KeywordFilterServiceImpl.new(@keywords.to_java :String)
filted = service.filter(comments.to_java :String)

forbiddens = service.getFiltedComments

assert forbiddens.length == comments.length - filted.length
assert_equal forbiddens.sort, (comments - filted).sort
end

其中,有两点需要注意,首先,我们可以通过to_java方法将Ruby类型转换成Java类型,例如上面将@keywords.to_java :String表明将Ruby数组转换成Java的String数组。第二,Ruby对数组支持“-”的操作,表示将一个数组减去和另一个数组中相同的元素,非常的直观!很明显,被过滤的数组应该等于原来的数组减去过滤后的结果!运行mvn test,我们将看到


[INFO] [jtestr:test {execution: default}]

Other TestUnit: 2 tests, 0 failures, 0 errors

结论

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


Ruby:
require 'test/unit'

import 'com.alisoft.research.JRuby.service.KeywordFilterServiceImpl'

class KeywordFilterServiceTest < Test::Unit::TestCase
def setup
@keywords = %w{X XX XXX XXXX XXXXX XXXXXX XXXXXXX}
end

def test_filter
comments = random_comments
service = KeywordFilterServiceImpl.new(@keywords.to_java :String)
filted = service.filter(comments.to_java :String)

forbiddens = service.getFiltedComments

assert forbiddens.length == comments.length - filted.length
assert_equal forbiddens.sort, (comments - filted).sort
end

def random_alphabetic(size=5)
chars = ('a'..'z').to_a + ('A'..'Z').to_a
(0...size).collect { chars[rand(chars.length)] }.join
end

def random_comments
comments ||= []
10.times do
keyword = rand(10) % 3 == 0 ? ' ' : @keywords[rand(@keywords.length)]
comment = random_alphabetic + keyword + random_alphabetic
comments << comment
end
return comments
end
end

Java:
package com.alisoft.research.JRuby.test;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

import Java.util.ArrayList;
import Java.util.Arrays;
import Java.util.List;

import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.junit.Test;

import com.alisoft.research.JRuby.service.KeywordFilterServiceImpl;

public class KeywordFilterServiceTest {

@Test
public void testFilteredResults() {
String[] comments = getRandomComments();

KeywordFilterServiceImpl service = new KeywordFilterServiceImpl(
getKeywords());

String[] filted = service.filter(comments);
String[] forbiddens = service.getFiltedComments();

assertEquals(filted.length + forbiddens.length, comments.length);

assertArrayEquals(forbiddens, sub(comments, filted));
}
//实现减法操作
private String[] sub(String[] all, String[] part) {
List allList = new ArrayList(Arrays.asList(all));
allList.removeAll(Arrays.asList(part));
return allList.toArray(new String[allList.size()]);
}

private String[] getRandomComments() {
String[] comments = new String[RandomUtils.nextInt(10)];
for (int i = 0; i < comments.length; i++) {
String comment = RandomStringUtils.randomAlphabetic(5);
String keyword = RandomUtils.nextBoolean() ? getKeywords()[RandomUtils
.nextInt(getKeywords().length)]
: "";
comment += keyword + RandomStringUtils.randomAlphabetic(5);
comments[i] = comment;
}
return comments;
}

private String[] getKeywords() {
String[] keywords = new String[] { "X", "XX", "XXX", "XXXX",
"XXXXX", "XXXXXX", "XXXXXXX" };
return keywords;
}

}

在借助了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中文站用户讨论组参与我们的线上讨论。

Ruby is good. 发表人 Chao Liu 发表于
Re: Ruby is good. 发表人 胡 键 发表于
Re: Ruby is good. 发表人 李 铮 发表于
Re: Ruby is good. 发表人 AnPing Yin 发表于
Re: Ruby is good. 发表人 Jian Johnny 发表于
Re: Ruby is good. 发表人 Chao Liu 发表于
Re: Ruby is good. 发表人 曹 云飞 发表于
不方便代码复用 发表人 Cai Pig 发表于
Ruby的这些特性的确很令人兴奋,但毕竟是一种新的语法 发表人 Lee Jack 发表于
Re: Ruby的这些特性的确很令人兴奋,但毕竟是一种新的语法 发表人 AnPing Yin 发表于
  1. 返回顶部

    Ruby is good.

    发表人 Chao Liu

    Groovy is just better.

  2. 返回顶部

    Re: Ruby is good.

    发表人 胡 键

    同意,对于Java开发者来说,Groovy学习更容易,且能取得同样的效果。

  3. 返回顶部

    Re: Ruby is good.

    发表人 李 铮

    Groovy is just better.


    I also think so

  4. 返回顶部

    Re: Ruby is good.

    发表人 AnPing Yin

    呵呵,萝卜青菜各有所好吧,只要是图灵完备的语言,理论上都是一样的,语言设计者的主要任务是努力提高开发的效率。

    groovy很好,可现在还没有成熟的杀手级的应用。其实ruby带给大家的不仅仅是一种语法糖衣,更重要的是它给了你一种全新的看待设计和开发的方式!这方面groovy很大程度上还不能带给我这样的惊喜,它更多的是在借鉴ruby和python这些动态语言。

  5. 返回顶部

    Re: Ruby is good.

    发表人 曹 云飞

    给出一些类似本文的例子更好,方便同学们学习提高

  6. 返回顶部

    Re: Ruby is good.

    发表人 Jian Johnny

    那不是很好吗?这样就集ruby和python的优点于一身了

  7. 返回顶部

    Re: Ruby is good.

    发表人 Chao Liu

    不错,老实说groovy还是靠ruby救活的,要不是有一帮子人受RoR的启发用groovy做出了grails,groovy也顶多是用来跑测试的。现在的groovy也一直在吸收ruby的使用经验。喜欢groovy完全是因为语法,ruby看起来就不美~~~如果非要用一种跟java不太一样的语言的话,我宁可用scala

  8. 返回顶部

    不方便代码复用

    发表人 Cai Pig

    很多情况下,单元测试的代码可以在正式代码中复用。
    在这个前提下,jruby方式的unit test就失去了优势
    但它的简洁性是不可否认的

  9. 返回顶部

    Ruby的这些特性的确很令人兴奋,但毕竟是一种新的语法

    发表人 Lee Jack

    十八般武艺都会的全才当然很好,但要求实在是太高了点,如果是测试人员跟开发人员各自独立工作还好说,但也产生了更多的沟通方面的问题,不利用实际开发过程中的定位和解决。

    我个人更倾向于开发和测试用同一种语言,方便互相检查。作者所说的一些便利性可以通过积累利用库来解决,如果都是临时性、一次性的东西,最好是单独安排专人来写测试代码。

  10. 返回顶部

    Re: Ruby的这些特性的确很令人兴奋,但毕竟是一种新的语法

    发表人 AnPing Yin

    学习一种新的语言或新的语法确实会有一个痛苦的过程,我记得我至少3次拿起过Ruby的书,又放下了。但是,超越了这个过程,给你带来的就是全新的感受!非常的棒!如果可能,我现在更愿意用Ruby去解决问题!测试工作可能是Ruby能为Java开发人员带来的最容易切入和实际好处之一。毕竟,不能学以致用的话,动力将会减少很多!
    我个人不是某种语言的宗教狂热着,但Ruby确实重新激起了我很多的激情!说实话,Java这几年带给我的类似的感觉越来越少。

深度内容

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

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

特性注入:成功三部曲

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