写点什么

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

评论

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

基于微前端qiankun的多页签缓存方案实践

vivo互联网技术

Vue 前端 微前端 qiankun

一体化在线政务服务平台,小程序容器技术加速建设步伐

Speedoooo

小程序 小程序容器 政务平台

流动性质押挖矿系统开发如何制作?单双币系统开发成熟技术

开发微hkkf5566

开源一夏 | GO语言框架中如何快速集成日志模块

Masters

开源

阿里云数据存储生态计划发布,助力伙伴数据创新

云桌派

阿里云

博云入选Gartner中国DevOps代表厂商

BoCloud博云

云计算 容器 DevOps 云原生 云平台

松哥手把手教你在 Vue3 中自定义插件

江南一点雨

Java Vue

STM32+MPU6050设计便携式Mini桌面时钟(自动调整时间显示方向)

DS小龙哥

8月月更

你认同这个观点吗?大多数企业的数字化都只是为了缓解焦虑

雨果

数字化转型 数字化

“纯C”实现——三子棋小游戏

一介凡夫

c 后端、 #开源 8月月更

SAP 云平台上一种 Low Code Development(低代码开发)解决方案

汪子熙

低代码 云平台 lowcode SAP 8月月更

1对1视频源码——快速实现短视频功能提升竞争力

开源直播系统源码

软件开发 直播系统源码 一对一视频聊天系统

开源一夏 | 深入理解 Spring Cloud Gateway 的原理

悟空聊架构

开源 Gateway 认证 签约计划第三季

10份重磅报告 — 展望中国数字经济未来

阿里技术

研究报告

OSI 七层模型和TCP/IP模型及对应协议(详解)

Five

TCP/IP TCP协议 8月月更

开源一夏|要有遥不可及的梦想,也要有脚踏实地的本事

坚果

开源 HarmonyOS OpenHarmony 8月月更

Gitee图床被屏蔽后,我搭建了一个文件系统并封装成轮子开源!

IT学习日记

Java springboot 签约计划第三季 seaweedfs文件系统 Gitee图床崩了

C++引用分析实例与案例刨析及使用场景分析详解

CtrlX

c c++ 后端 程序员进阶 8月月更

外包学生管理系统架构文档

Maven中的scope

六月暴雪飞梨花

maven 开源 技术 8月月更 scope

mysql进阶(二十一)删除表数据与数据库四大特性

No Silver Bullet

MySQL 8月月更 四大特性 表删除

转转反爬攻防战

转转技术团队

爬虫

Geoffrey Hinton:深度学习的下一个大事件

OneFlow

人工智能 神经网络 深度学习

C++关于函数参数的分析与函数重载进阶教程

CtrlX

c++ 编程语言 后端 cpp 8月月更

如何在技术上来保证LED显示屏质量?

Dylan

LED显示屏 led显示屏厂家

【LeetCode】设计数字容器系统Java题解

Albert

LeetCode 8月月更

爆款视频怎么做?这里或许有答案!

博文视点Broadview

kafka_2.13-3.2.0.tgz配置安装

Cjpler

签约计划第三季

重磅大咖来袭!阿里云生命科学与智能计算峰会精彩内容剧透

阿里云弹性计算

高性能计算 生命科学 AI制药 智能计算

DVWA 通关记录 2 - 命令注入 Command Injection

Todd-Lee

阿里前端智能化技术探索和未来思考

阿里技术

前端 智能化

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