单元测试正是敏捷方法的核心所在。
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方法,提取到所需要的日志信息并进行最终校验。
-
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站点或者主页去了解更多。