HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

评论

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

厉害了!把 HashMap 剖析的只剩渣了!

爱好编程进阶

Java 程序员 后端开发

编写自己的 WordPress 模板

海拥(haiyong.site)

WordPress 5月月更

【刷题第十天】21. 合并两个有序链表

白日梦

5月月更

摆平各类目标检测识别AI应用,有它就够了!

华为云开发者联盟

计算机视觉 CANN 昇腾 目标检测识别 YoloV3

“超级计算机”——GPU云服务器

Finovy Cloud

gpu GPU服务器 GPU算力

云计算的云是指什么?最简单的解释是什么?

行云管家

云计算 云服务 私有云 混合云

HTTP请求转发那些事:你可能不知道的Hop-by-hop Headers和End-to-end Headers

华为云开发者联盟

HTTP 请求转发 web 容器 F5

小型企业需要CRM系统的理由

低代码小观

CRM 企业管理系统 中小企业 CRM系统 客户关系管理系统

「每日一问」并发编程的特性是什么

爱好编程进阶

Java 程序员 后端开发

【Jvm】Jvm类加载机制

爱好编程进阶

Java 程序员 后端开发

【解决】jsPDF之长图片生成PDF

爱好编程进阶

Java 程序员 后端开发

极光笔记 | 极光基于元数据驱动数据治理浅谈

极光JIGUANG

数据治理 元数据

以太坊L2跨链明星——Arbitrum 为开发者带来区块链入门方案

TinTinLand

区块链

KeyDB重量发布6.3.0开源版

华为云开发者联盟

redis 开源 多线程 分布式缓存 KeyDB

AI简报-视频去交错(Deinterlance)

AIWeker

人工智能 深度学习 5月月更 去交错

【直播预告】程序员修炼之道——如何实现敏捷开发与稳健运行的有机统一

FinClip

程序员 小程序运营

「2021-老胡的年终总结」

爱好编程进阶

程序员 后端开发

史上最全图解JVM,京东小哥带你了解性能提升100%背后的秘密武器

爱好编程进阶

Java 程序员 后端开发

大佬总结的4条宝贵经验,送给初入职场的你,从此一飞冲天

爱好编程进阶

Java 程序员 后端开发

JAVA-使用注解实现Excel表头多语言导出

JayJay

5月月更

等保和分保的区别是什么?哪个更厉害?

行云管家

网络安全 等保 等级保护 分保

第三代区块链DFINITY布局DeFi,开发者如何抓住机遇

TinTinLand

区块链

如何在缺乏商业项目经验的前提下成功通过面试,兼说我如何甄别非商业项目经验

爱好编程进阶

Java 程序员 后端开发

Tomcat介绍(三)

爱好编程进阶

程序员 后端开发

优化了MYSQL大量写入问题,老板奖励了1000块给我

爱好编程进阶

Java 程序员 后端开发

使用APICloud AVM多端框架开发企业移动OA办公的项目实践

YonBuilder低代码开发平台

企业应用 APP开发 APICloud 多端开发 avm.js

讨论两种Redis中Token的存储方式

华为云开发者联盟

Token key 存储token userid

Hacker 资讯 | 5 月上旬区块链黑客松活动汇总

TinTinLand

区块链

令人欣慰!阿里大牛把Spring微服务架构设计整理成了500多页文档,真香

爱好编程进阶

Java 程序员 后端开发

一文聊透 Netty IO 事件的编排利器 pipeline | 详解所有 IO 事件的触发时机以及传播路径

bin的技术小屋

网络编程 Netty java netty 5月月更

招募 | 加入DFINITY 进阶开发,人人都是下一个“张一鸣”

TinTinLand

区块链

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