Java程序员为什么学习Groovy(测试组件)

单元测试正是敏捷方法的核心所在。

1.测试是可执行的代码范例,即使文档有过期的风险,测试则会紧跟代码,因为这是编译器强制保证的
2.此外,有了单元测试,就意味着开发者能够持续的对代码进行大胆的重构而无需付出太大的测试成本和风险,进而提高代码的健壮性和系统质量,达到程序员最终的解放。

在Java的世界里,JUnit则是引领这个潮流的弄潮儿,是Java程序员开发工具箱里的必备物资,而对于Groovy而言,测试同样重要,鉴于和Java同根同源的现状,Groovy采用了拿来主义的思想,其测试的第一步就是继承JUnit的父类,完成一系列的增强,让其更加好用(事实上,这句话几乎就是Groovy对待Java的“遗产”上的一贯处理方式),下面我们来做具体了解:
1.Groovy可以和JUnit3一样,继承junit.framework.TestCase抽象类来构造测试。除了一些语法糖,基本用法和Java一样。
2.Groovy可以和JUnit4一样,使用annotation来标记测试方法,@Test,因为JUnit4不要求用户继承固定的父类,所以一般的处理方式是通过静态引入加入Assert这个类,调用其静态的断言方法来实现单元测试的需要。
3.可以继承Groovy自己实现的测试父类groovy.util.GroovyTestCase来实现单元测试,在这个父类里,包含了众多方便的断言方法。比如assertLength和shouldFail等。当然,这种靠继承的方式来实现的测试方法被识别出来的规则和JUnit3一样:方法需要是public,无返回值的,且名字必须以test开头才行。

class LabTest extends GroovyTestCase{
     public void testArrayEquals() {
        def mm =["a", "b"] as String[]
        assertArrayEquals(mm, ["a", "b"] as String[])
     }
     public void testLength(){
         assertLength(3, ["c", "b"] as String[])
     }
}

结果为:

  • junit.framework.AssertionFailedError: expected:<3> but was:<2> *

4.脚本测试,Groovy作为一种便于脚本的语言,脚本测试的便利性也很重要。执行脚本测试,可以使用GroovyShell类来编程式的调用脚本来执行。具体如下面的例子:

import groovy.json.JsonSlurper
def jsonResult = "http://m.weather.com.cn/data/101290401.html".toURL().getText()
def jsonParser = new JsonSlurper().parseText(jsonResult)
jsonParser?.weatherinfo?.with {
println "temp: $temp1 @ city: $city"    
}

这个例子打印某个城市的天气,可以获得一个Json的返回值,大致如下:
<pre>
{
"weatherinfo": {
"city": "哈尔滨",
"temp1": "-5℃~-17℃"
}
}
</pre>

篇幅所限,这里精简了大部分的结果,只保留了我们脚本需要输出的部分,对于这个脚本,我们需要在使用它之前对它进行合理的测试,我们试试刚说到的GroovyShell类

import static org.junit.Assert.*;
import org.junit.Test;
class GetWeatherInfoTest{
@Test
  public void test() {
  GroovyShell shell = new GroovyShell()
  shell.evaluate(new File("src/groovytestlab    /GetWeatherInfo.groovy"));
  }
}

上面的只是可以编程式的执行脚本,但是对于脚本做断言,我们还需要能够拦截到脚本执行的时候的标准输出,对于GroovyShell的话,指定一个Binding对象就可以做到,改善后的代码如下:

package groovytestlab;
import static org.junit.Assert.*;
import org.junit.Test;
class GetWeatherInfoTest{
    @Test
    public void test() {
        Binding binding = new Binding()
        def content = new StringWriter()
        binding.out = new PrintWriter(content)
        GroovyShell shell = new GroovyShell(binding)
        shell.evaluate(new File("src/groovytestlab/GetWeatherInfo.groovy"));
        assert content.toString().trim() =~ /temp:\s*\d+℃~\d+℃ @ city: \W+/
    }
}

如此一来就可以拦截到Groovy脚本的输出并作出校验了,我们还能更进一步,继承Groovy提供的GroovyShelTestCase来构件测试,这个类继承自GroovyTestCase,其隐式的包含了一个protected成员shell,即上例中所说的GroovyShell,完成后的代码如下:

class GetWeatherInfoTest extends GroovyShellTestCase{
@Test
public void test() {
    def content = new StringWriter()
    withBinding(out:new PrintWriter(content)) {
        shell.evaluate(new File("src/groovylab/GetWeatherInfo.groovy"));
        assert content.toString().trim() =~ /temp:\s*\d+℃~\d+℃ @ city: \W+/
    }
}
}

和上面一个差别不大,不过是在已有具备shell的背景下,我们可以在withBinding方法里,以Map的方式把需要指定的标准输出放进去,在withBinding的闭包作用域里,我们都能自由的使用这些设定。
5.日志测试子类GroovyLogTestCase
被测试类:

class LogTestLab {
def log = Logger.getLogger(this.class.name)
int plus(int a,int b){
    int z = a+b
    log.info("result is ${z}")
    z
}
}

测试类:

class LogTest extends GroovyLogTestCase{
LogTestLab labtest = new LogTestLab();
void testLog(){
    String log = stringLog(Level.INFO, LogTestLab.class.name) {
        labtest.plus(3, 2)
    }
    assert log.contains("result is 5")
}
}

对于这类型的子类来说,关键在于能够通过父类提供的stringLog方法,提取到所需要的日志信息并进行最终校验。

  1. as
    用过swing的人都知道,如果需要增加一个鼠标事件,有两种方式,一种是实现MouseListener接口,必须实现接口里面如mouseClicked,mousePressed等5个方法才行,而实际上我们可能只需要其中一两个,此时一般我们会换用另一种更省事的方式,即去继承一个叫MouseAdapter的适配器类,这个抽象类也继承于MouseListener,已经帮我们空实现了所有的监听方法,我们只要覆写自己需要的方法即可。提到这个,是因为在测试中,我们也往往面临这种处境,为了保证测试的隔离,我们往往需要mock出多种不同的对象,而这个对象也许在业务中我们只需要用到它的很少一部分特性,只需要构建我们需要的部分,其它留给Groovy来填补,这会是一个不错的主意。
    一个学生类:

    class Student {
    StudentListener listener;
    String listenerState
    void study(){
    if(listener){
    listenerState = listener.studentsStudy()
    }
    }
    }
    一个监听学生行为的接口:

    public interface StudentListener {
    public String studentsLeave();
    public String studentsCome();
    public String studentsStudy();
    public String studentsSleep();
    }
    我们现在要写一个测试,来看一下当学生学习的时候,监听接口的studentsStudy接口是否已经被调用,这里我们只需要测试接口里的这个方法。

    import org.junit.Test
    class StudentStudyTest {
    Student stu = new Student()
    @Test
    public void testStudyEvent(){
    stu.listener={ "students start study"} as StudentListener
    assert stu.listenerState == null
    stu.study()
    assert stu.listenerState=="students start study"
    }

这里的代码清楚的显示,利用as关键词,groovy用一个闭包就mock出了StudentListener的对象,而且能够提供合适的行为。也许读者要问,这里并没有告诉它我们需要实现哪个方法呀,它是如何把闭包的实现和我们需要的方法绑定的呢?我们可以在Student里再实现一个come方法,调用监听器的studentsCome()方法,检查stu的状态我们会发现,被执行的依然是我们闭包里的代码。
这就是全能小王子闭包,它无处不在。但是我们有时候的确需要实现多于1个的实现,这时候我们可以用Map的方式来指定我们方法和闭包实现之间的关系。如下所示。

    stu.listener = [studentsStudy:{"students start study"}, studentsCome:{"students start come"}] as StudentListener
    assert stu.listenerState == null
    stu.study()
    assert stu.listenerState=="students start study"
    
    stu.listenerState = null
    
    stu.come()
    assert stu.listenerState == "students start come"

6.Expando
使用Groovy的时候,无处不在的是它强大的MOP特性,这里展示它用代码来创建一个mock对象的过程,强大的秘诀就在于Expando这个类。
下面我们来具体看下Expando的一个小例子:

def ex = new Expando()
ex.name = 'XiaoMing'
ex.speak = { "$name says Hello World!" }
println ex.speak()

我们可以看出,Expando的创建过程和使用asm工具修改字节码产生一个Java对象类似,但是两者绝不相同,Expando只是创建了一个形似之物,借助于Groovy的动态类型,就成功的完成了mock过程。
7、StubFor和MockFor
Stub和Mock,是测试领域经常拿出来辨析的两个概念,两者在很多地方使用的界限也比较模糊,按照我的理解,Stub大致就是一个简单的实现,充当实际业务的“替代者”,而Mock则纯粹就是无中生有来替代依赖系统的。不过在Groovy里的两个相关类又不一样:两者都有“替代”作用,而StubFor对交互的约定没有MockFor这么强烈。
下面以给String类增加两个不存在的模拟方法,来说明两个类的用法:

def stubfor = new StubFor(String.class)  
stubfor.demand.one(1..2) { 1 }               
stubfor.demand.two()     { 2 }      
stubfor.use {
def caller = new String()               
assertEquals 2, caller.two()  
    assertEquals 2, caller.one()  
}

StubFor主要构建是demand和use两个方法的使用,一个指定了需要被模拟的方法和具体的实现过程,实现过程用闭包来处理;而use则打开一个闭包,在闭包的上下文里,demand指定的方法完全的生效,demand闭包括号里的(1..2)不是方法参数,而是表示这个方法可以被调用的次数是1次或者2次。读者可以通过增减use里面的断言判断,来找到subFor的规则约束所在:
(1)use代码块里,demand指定的方法调用次序必须是和声明的一致。
(2)use代码块里,demand指定的方法不一定都要调用到,不过如果调用到,调用次数不能超过定义的次数。

def mocker = new MockFor(String.class)  
mocker.demand.one(1..2) { 1 }                
mocker.demand.two()     { 2 }     
mocker.use {
   def caller = new String()                 
   assertEquals 1, caller.one()   
   assertEquals 1, caller.one()  
   assertEquals 2, caller.two()  
}

MockFor的用法和SutbFor是一致的,无需赘言,这里注意下,在MockFor的demand里定义了的方法,则必须顺序和次数都必须严格在use里出现,否则就视为错误,这是和StubFor的主要不同。

8.Spock-下一代测试框架
在测试方法,最有资格代表Groovy系列出战的组件就是Spock了,它对应于Java中的JUnit,其灵活的编码方式,自解释的case类型,都让人耳目一新,下面做一个简单的介绍。
使用Spock的测试都必须继承spock.lang.Specification这个类,具体的方法格式是:
<pre>
def 描述性文字(){
语法关键词:语法块
}
</pre>

一个很直观的例子是:

import spock.lang.Specification
class SampleSpec extends Specification {
String quote = """I am endeavoring, ma'am, to construct a mnemonic memory circuit, using stone knives and bear skins."""
List<String> strings
def setup() {
    strings = quote.tokenize(" ,.")
}

def "测试list是不是长度为16"() {
    expect: strings.size() == 16
}
def "我加一个单词进去看看长度是不是加了1"() {
    when: strings << 'Fascinating'
    then: strings.size() == old(strings.size()) + 1
}
}

这个例子给我们很多的信息量,比如spock的测试前执行方法名字是setup,比如描述性语句是如此个性化,写成这样那注释也没必要写了,语句块的expect和then接受的都是boolean值的表达式,而when则是设置条件的,里面比较怪异的old方法,居然可以获取到改变之前的队列值。接下来再来一个例子,说明spock里数据驱动类型的用例。

class SampleSpec2 extends spock.lang.Specification {
    @Unroll
def "#name should have #length"(String name, int length) {
    expect:
    name.size() == length
    where:
    name     | length
    "Spock"  | 5
    "Kirk"   | 4
    "Scotty" | 6
}
def "check lengths using arrays"() {
    expect: name.size() == length
    where:
    name << ["Spock","Kirk","Scotty"]
    length << [5,4,6]
}
@Unroll
def "check #length using #name pairs"() {
    expect: name.size() == length
    where:
    [name,length] << [["Spock",5],["Kirk",4],["Scotty",6]]
}
}

这个例子展示的是where关键词的用法,里面可以放入三种不同类型的数据集,然后以迭代的方式逐一进行测试,读者需要注意这么几个地方。
@unroll注解可以平铺开每个数据项,使测试结果更加好看,比如第一个测试用例执行后我们可以看到:
<pre>
Spock should have 5(0.000s)
Kirk should have 4(0.000s)
Scotty should have 6(0.000s)
</pre>
而如果去掉@unroll,则看到
<pre>
#name should have #length
</pre>
进而可以了解到,@unroll,还给描述性语句带来了变量替换的功能。
另外,例子中第一种描述方式为表输入描述。第一行叫做表头,是类似于方法形参的东西,表头以外的行的数据都会依次替换到对应表头代表表达式里面去,表结构方式表至少需要两列,如果只有一个变量的话,第二列可以使用_来作为替换。
另外两个用例则都是容器数据注入的方式,可以根据需要灵活决定。
这里对spock只做了简单介绍,更多的东西,可以到spock的github站点或者主页去了解更多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容